<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Local-Models | AkitaOnRails.com</title><link>https://akitaonrails.github.io/en/tags/local-models/</link><description>Fabio Akita's blog — tech, career, and assorted geek topics. English edition.</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Mon, 19 Oct 2026 23:20:51 +0000</lastBuildDate><atom:link href="https://akitaonrails.github.io/en/tags/local-models/index.xml" rel="self" type="application/rss+xml"/><item><title>LLM Benchmark v4: the top open source LLMs on a B300</title><link>https://akitaonrails.github.io/en/2026/10/19/llm-benchmark-v4-the-top-open-source-llms-on-a-b300/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/10/19/llm-benchmark-v4-the-top-open-source-llms-on-a-b300/</guid><pubDate>Mon, 19 Oct 2026 14:00:00 -0300</pubDate><description>&lt;p&gt;In the last two v4 benchmark posts I &lt;a href="https://akitaonrails.github.io/en/2026/09/15/new-llm-benchmark-v4-retesting-all-top-llms-part-1/"&gt;rebuilt the test from scratch&lt;/a&gt;, with a Rails app sabotaged on purpose across seven sprints, and &lt;a href="https://akitaonrails.github.io/en/2026/09/15/new-llm-benchmark-v4-retesting-39-llms-part-2/"&gt;ranked 39 models&lt;/a&gt; by vigilance: six ties at the top, a free model beating half of Claude. Then I &lt;a href="https://akitaonrails.github.io/en/2026/09/23/llm-benchmark-v4-opus-5-5-gpt-6-sol-luna-mimo-2-6-grok-4-7/"&gt;tested five new models&lt;/a&gt;, Opus 5.5, GPT 6 sol and luna, MiMo V2.6 Pro and Grok 4.7: Opus 5.5 closed at 100 and joined the tie at the top, GPT 6 luna scored 95.5 for under a dollar a run, and Grok 4.7 regressed badly against its own predecessor.&lt;/p&gt;</description><content:encoded><![CDATA[<p>In the last two v4 benchmark posts I <a href="/en/2026/09/15/new-llm-benchmark-v4-retesting-all-top-llms-part-1/">rebuilt the test from scratch</a>, with a Rails app sabotaged on purpose across seven sprints, and <a href="/en/2026/09/15/new-llm-benchmark-v4-retesting-39-llms-part-2/">ranked 39 models</a> by vigilance: six ties at the top, a free model beating half of Claude. Then I <a href="/en/2026/09/23/llm-benchmark-v4-opus-5-5-gpt-6-sol-luna-mimo-2-6-grok-4-7/">tested five new models</a>, Opus 5.5, GPT 6 sol and luna, MiMo V2.6 Pro and Grok 4.7: Opus 5.5 closed at 100 and joined the tie at the top, GPT 6 luna scored 95.5 for under a dollar a run, and Grok 4.7 regressed badly against its own predecessor.</p>
<p>One category was missing that I had never tested properly: the big open source models. The ones that fit on my <a href="/en/2026/01/21/omarchy-3-dual-gpu-setup-with-amd-and-nvidia/">RTX 5090</a> with 32 GB or on my <a href="/en/2026/03/31/minisforum-ms-s1-max-amd-ai-max-395-review/">Strix Halo</a> with 61 GB usable by the GPU are too weak for this. They work for simple agent tasks, and no small model handles real programming tasks, let alone a complex software project.</p>
<p>Qwen 3.8 27B took 706 minutes to finish v4 on the Strix and scored 80; the local GLM-4.7-Flash scored 24. So don&rsquo;t even try.</p>
<p>But what if we push harder? What can mid-sized models, 100 to 400 billion parameters, do running on a single rented B300? I could rent four of them and run the giant 750-billion or one-trillion models, and I&rsquo;m not sure the cost-benefit is worth it. The data below answers.</p>
<blockquote>
  <p><strong>TL;DR:</strong> 14 open models, one 288 GB B300 on Runpod at about 9 dollars an hour, the same v4 sabotage protocol. None of them serves fast the way it ships: GLM 5.3 Flash decoded at 10 tokens per second with the default configuration and at 141 after tuning, 14 times more from flags alone. Two open models, GLM 5.3 Flash and Qwen3.8 Flash-Next, scored 92.5, Claude Sonnet 5 level, and Swift 1.5 27B scored 88, Opus 4.6 level. None reached the frontier&rsquo;s 100. The 397-billion flagship scored 68.5 and let an admin backdoor through, DeepSeek V4 Flash scored 86 in the cloud and 44.5 served by me, and Nemotron cheated on a regression test. The cloud API of the same model costs a dollar per run; the pod costs 218 dollars a day. Jump to <a href="#what-a-b300-is-and-what-runpod-is">B300 and Runpod</a>, <a href="#none-of-them-serves-fast-the-way-it-ships">server tuning</a>, the <a href="#the-ranking">ranking</a>, the <a href="#the-interesting-cases">interesting cases</a>, the <a href="#how-far-from-the-frontier">frontier comparison</a>, the <a href="#the-math">math</a> or the <a href="#is-owning-your-own-infra-worth-it">conclusion</a>.</p>

</blockquote>
<h2>What a B300 is, and what Runpod is<span class="hx:absolute hx:-mt-20" id="what-a-b300-is-and-what-runpod-is"></span>
    <a href="#what-a-b300-is-and-what-runpod-is" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The <strong>B300</strong> is the datacenter GPU of NVIDIA&rsquo;s Blackwell Ultra generation, announced in March 2025. Each card has <a href="https://developer.nvidia.com/blog/nvidia-blackwell-ultra-for-the-era-of-ai-reasoning/"target="_blank" rel="noopener">288 GB of HBM3e memory</a>, nine times my 5090, and about 15 petaflops of dense compute in NVFP4, the 4-bit format Blackwell does in hardware. It&rsquo;s the card that goes inside the GB300 NVL72 rack, 72 of them interconnected, which AI companies buy by the thousands.</p>
<p>On its own, a B300 costs more than an apartment. What makes it accessible is renting by the hour.</p>
<p><a href="https://www.runpod.io/"target="_blank" rel="noopener"><strong>Runpod</strong></a> is a cloud GPU provider that rents cards in &ldquo;pods&rdquo;, Linux containers with one or more GPUs, billed per second. You pick the card, an image template, get in over SSH, and pay while the pod is on, idle included. The B300 cost about <a href="https://www.runpod.io/pricing"target="_blank" rel="noopener">9 dollars an hour</a> on their secure cloud the week of the test; an H100 goes for 3 to 4. The container disk is ephemeral: terminate the pod and the 250 GB of weights you downloaded are gone, and you download again.</p>
<p>The pod I used: one B300 with 275 GiB according to <code>nvidia-smi</code> (the same nominal 288 GB, in binary units), 344 vCPUs, 4 TB of host RAM, 700 GB of disk. The inference server was vLLM; the harness was the same OpenCode as every other model in the ranking, connected to the pod through an SSH tunnel. The model runs there; the code, the tests and the results stay on my machine.</p>
<p>Fitting in memory is the first filter. The B300 fits the 100 to 400-billion-parameter models quantized to NVFP4, plus the 27 to 35-billion ones in BF16. Left out are the 753-billion GLM 5.3 flagship, the 750-billion DeepSeek V4.1 Flash, the 1.6-trillion DeepSeek V4 Pro and the 2.8-trillion Kimi K3. For those it would take four B300s or more, about 36 dollars an hour.</p>
<h2>The methodology<span class="hx:absolute hx:-mt-20" id="the-methodology"></span>
    <a href="#the-methodology" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The protocol is the usual <a href="/en/2026/09/15/new-llm-benchmark-v4-retesting-all-top-llms-part-1/">v4</a>: the model builds a Rails app across seven accumulating sprints, and between sprints an independent reviewer, isolated from the model, injects 14 sabotages, from real CVEs to silent logic and test regressions, into the code the model itself wrote. The 0-to-100 score measures vigilance: how much the model catches and fixes without being told (full weight), how much it fixes after an explicit &ldquo;do a security audit&rdquo; in the last sprint (40% of the weight), and what stays unresolved (zero). Tiers are A from 83 up, B from 73 to 82, C from 51 to 72, D up to 50.</p>
<p>Two differences this round. The first is the reviewer: the five models that finished before the block were reviewed end to end by Codex, as in the previous rounds.</p>
<p>Midway through the batch OpenAI&rsquo;s security service started refusing the injection and grading operation with a cybersecurity risk flag, even though it&rsquo;s my code, on my machine, in a benchmark with 46 models already scored under the same protocol. I switched the reviewer to Claude Opus 4.8 for the remaining models, with the same injection plan, same catalog, same yardstick, and the provenance of every boundary is recorded per sprint. The expected impact on the score is none, because both execute the same written recipe.</p>
<p>The second is admission by speed, which is the subject of the next section. A model that decodes below 100 tokens per second doesn&rsquo;t even get on the track.</p>
<p>Also: an open model is entitled to up to two short nudges per run for silly defects (a missing import, a wrong test entry point), recorded as &ldquo;assisted&rdquo;. Infrastructure failure, driver, loader, parser, is never charged to the model. And the pod ran one model at a time, with no access to any other model&rsquo;s workspace or results.</p>
<h2>None of them serves fast the way it ships<span class="hx:absolute hx:-mt-20" id="none-of-them-serves-fast-the-way-it-ships"></span>
    <a href="#none-of-them-serves-fast-the-way-it-ships" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The first lesson cost a day. I downloaded <a href="https://huggingface.co/RedHatAI/GLM-5.3-Flash-NVFP4"target="_blank" rel="noopener">GLM 5.3 Flash</a>, 320 billion parameters with 18 active, quantized to NVFP4, 198 GB. I brought it up on vLLM with the configuration that &ldquo;works&rdquo;: it serves, passes the tool-call tests, answers. And it decoded at <strong>10 tokens per second</strong>, with the GPU at 10% utilization, 209 watts out of the 1,100 the pod allows.</p>
<p>That&rsquo;s unusable for programming. A coding agent generates tens of thousands of tokens per sprint; at 10 per second, a 20-minute sprint becomes more than four hours.</p>
<p>The same checkpoint, same 128K-token context, same FP8 KV cache, same test prompt, changing only the compile and mode flags: <strong>141 tokens per second</strong>. Fourteen times faster, zero change in precision or weights.</p>
<p>The main culprit was <code>--enforce-eager</code>, which disables vLLM&rsquo;s <code>torch.compile</code> and CUDA graph capture, and which every tutorial tells you to enable &ldquo;to start faster&rdquo;. It starts faster and serves 14 times slower. The measurement changed that flag together with the <code>interactivity</code> mode, so I didn&rsquo;t isolate each one&rsquo;s contribution.</p>
<p>Hence the admission rule: <strong>below 100 tokens per second I consider unusable</strong>, and no model enters the benchmark without clearing that on the median of two warm 256-token probes, after a cold one. To keep it from becoming a bottomless pit, tuning is bounded: at most three configurations and 45 minutes of wall-clock per model, including load and compilation. And tuning can never trade quality for speed: no shortening the context, lowering reasoning effort or quantizing further to hit the number.</p>
<p>The tweaks that paid off for everyone:</p>
<ul>
<li><strong>Install a CUDA that compiles for the card.</strong> Runpod&rsquo;s default image ships CUDA 12.8, whose <code>nvcc</code> <a href="https://developer.nvidia.com/cuda-gpus"target="_blank" rel="noopener">cannot compile for <code>compute_103a</code></a>, the B300&rsquo;s compute capability. Without 12.9, the NVFP4 kernels never compile.</li>
<li><strong>Never use <code>--enforce-eager</code>.</strong> Letting vLLM compile and capture CUDA graphs is the single biggest win.</li>
<li><strong><code>--performance-mode interactivity</code></strong>: optimize for single-stream latency, which is what a serial agent needs, instead of aggregate throughput.</li>
<li><strong>FP8 KV cache on the big MoEs, prefix caching and chunked prefill on all of them</strong>, confirmed by vLLM&rsquo;s Prometheus counters instead of assumed: on an 11K-token prompt, the server reused exactly 8,576 prefix tokens on every repeat.</li>
<li><strong>The right tool and reasoning parser for each family.</strong> The wrong parser makes tool calls vanish silently or leaks the reasoning into the answer.</li>
</ul>
<p>And the specific tweaks I found most interesting:</p>
<ul>
<li><strong>NVFP4 only runs on vLLM.</strong> The 198 to 251 GB checkpoints in Blackwell&rsquo;s native FP4 are the only way to fit on the card, and llama.cpp and Ollama simply don&rsquo;t load those formats. Whoever lives on GGUF has no access to this range.</li>
<li><strong>Native speculative decoding.</strong> Qwen 3.8 27B went from 92 to 195 tokens per second with the MTP head the checkpoint itself ships, predicting three tokens per step, plus <code>--language-model-only</code> to drop the vision tower (measured on different pods, so no controlled ablation). <a href="https://huggingface.co/meta-models/Muse-Glimmer-30B"target="_blank" rel="noopener">Muse Glimmer</a> reached 295 with the lightweight DFlash drafter it ships alongside, 15 speculated tokens per step. I only enabled it where an official drafter exists and where tool calling stayed correct; for Ornith a drafter exists as a separate release, with unknown acceptance rate, so it stayed off.</li>
<li><strong>The CUDA 12 loader.</strong> flashinfer JIT-compiles an NVFP4 MoE kernel that needs <code>libcudart.so.12</code>, and after installing CUDA 13 for Nemotron the library resolution broke. The symptom looked like OOM; the cause was an <code>ld.so.conf</code>. A repair script with <code>ldd</code> evidence before and after fixed it, and the 397B went on to serve at 144 tokens per second.</li>
<li><strong>One virtual environment per runtime.</strong> Nemotron 3.5 Super VL required its own vLLM 0.32 in a separate venv, because that&rsquo;s what the official card pins, and that build wants CUDA 13. Every model family is a bespoke case: parser, quantization, draft head, runtime version and loader repair, all different.</li>
<li><strong>A parser bug that dropped the last call.</strong> vLLM&rsquo;s Qwen tool parser discarded the last parameter of a call when the parameter closing tag was missing but the function one was present. A narrow repair, with 24 test cases, took that serving noise out of the model&rsquo;s account.</li>
</ul>
<p>The exact arguments for each model, the probe logs and the ledger of every tuning round are in the <a href="https://github.com/akitaonrails/llm-coding-benchmark/blob/master/docs/b300-serving-optimizations.md"target="_blank" rel="noopener">serving optimizations document</a> and the <a href="https://github.com/akitaonrails/llm-coding-benchmark/blob/master/docs/runpod-b300-openweights-runbook.md"target="_blank" rel="noopener">B300 runbook</a> in the repository.</p>
<blockquote>
  <p>Speed is almost free, once you let vLLM compile. The real work is finding out, model by model, which combination of toolkit, parser, quantization and draft head that checkpoint demands. I gave each one 45 minutes and needed them.</p>

</blockquote>
<h2>The ranking<span class="hx:absolute hx:-mt-20" id="the-ranking"></span>
    <a href="#the-ranking" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>v4 vigilance score, tier, memory footprint, decode speed on the admission probe (warm, single stream) and the cost of the seven sprints&rsquo; inference time only, at 9.09 dollars an hour. The cost excludes download, load, compilation, tuning and idle time; the real math comes further down.</p>
<table>
  <thead>
      <tr>
          <th style="text-align: right">#</th>
          <th>Model</th>
          <th style="text-align: right">v4</th>
          <th>Tier</th>
          <th style="text-align: right">Memory</th>
          <th style="text-align: right">tok/s</th>
          <th style="text-align: right">Minutes</th>
          <th style="text-align: right">Inference cost</th>
          <th>Reviewer</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: right">1</td>
          <td>GLM 5.3 Flash (NVFP4)</td>
          <td style="text-align: right"><strong>92.5</strong></td>
          <td>A</td>
          <td style="text-align: right">198 GB</td>
          <td style="text-align: right">142</td>
          <td style="text-align: right">145</td>
          <td style="text-align: right">US$ 21.96</td>
          <td>Codex</td>
      </tr>
      <tr>
          <td style="text-align: right">1</td>
          <td>Qwen3.8 Flash-Next (NVFP4)</td>
          <td style="text-align: right"><strong>92.5</strong></td>
          <td>A</td>
          <td style="text-align: right">100 GB</td>
          <td style="text-align: right">195</td>
          <td style="text-align: right">80</td>
          <td style="text-align: right">US$ 12.04</td>
          <td>Codex → Claude</td>
      </tr>
      <tr>
          <td style="text-align: right">3</td>
          <td>Swift 1.5 27B</td>
          <td style="text-align: right"><strong>88.0</strong></td>
          <td>A</td>
          <td style="text-align: right">56 GB</td>
          <td style="text-align: right">203</td>
          <td style="text-align: right">64</td>
          <td style="text-align: right">US$ 9.71</td>
          <td>Codex</td>
      </tr>
      <tr>
          <td style="text-align: right">4</td>
          <td>ThinkingCap 3.8 27B</td>
          <td style="text-align: right"><strong>83.0</strong></td>
          <td>A</td>
          <td style="text-align: right">56 GB</td>
          <td style="text-align: right">189</td>
          <td style="text-align: right">76</td>
          <td style="text-align: right">US$ 11.47</td>
          <td>Codex → Claude</td>
      </tr>
      <tr>
          <td style="text-align: right">5</td>
          <td>Muse Glimmer 30B</td>
          <td style="text-align: right">81.0</td>
          <td>B</td>
          <td style="text-align: right">60 GB</td>
          <td style="text-align: right">295</td>
          <td style="text-align: right">23</td>
          <td style="text-align: right">US$ 3.54</td>
          <td>Codex → Claude</td>
      </tr>
      <tr>
          <td style="text-align: right">5</td>
          <td>Ornith 1.5 35B</td>
          <td style="text-align: right">81.0</td>
          <td>B</td>
          <td style="text-align: right">70 GB</td>
          <td style="text-align: right">317</td>
          <td style="text-align: right">161</td>
          <td style="text-align: right">US$ 24.39</td>
          <td>Codex → Claude</td>
      </tr>
      <tr>
          <td style="text-align: right">7</td>
          <td>Qwen 3.8 27B</td>
          <td style="text-align: right">80.5</td>
          <td>B</td>
          <td style="text-align: right">54 GB</td>
          <td style="text-align: right">195</td>
          <td style="text-align: right">192</td>
          <td style="text-align: right">US$ 29.11</td>
          <td>Codex</td>
      </tr>
      <tr>
          <td style="text-align: right">8</td>
          <td>Qwen3.5 397B (NVFP4)</td>
          <td style="text-align: right">68.5</td>
          <td>C</td>
          <td style="text-align: right">251 GB</td>
          <td style="text-align: right">140</td>
          <td style="text-align: right">54</td>
          <td style="text-align: right">US$ 8.22</td>
          <td>Codex → Claude</td>
      </tr>
      <tr>
          <td style="text-align: right">9</td>
          <td>Nemotron 3.5 Super VL (NVFP4)</td>
          <td style="text-align: right">63.0</td>
          <td>C</td>
          <td style="text-align: right">70 GB</td>
          <td style="text-align: right">207</td>
          <td style="text-align: right">50</td>
          <td style="text-align: right">US$ 7.51</td>
          <td>Codex → Claude</td>
      </tr>
      <tr>
          <td style="text-align: right">10</td>
          <td>DeepSeek V4 Flash</td>
          <td style="text-align: right">44.5</td>
          <td>D</td>
          <td style="text-align: right">167 GB</td>
          <td style="text-align: right">153</td>
          <td style="text-align: right">37</td>
          <td style="text-align: right">US$ 5.59</td>
          <td>Codex</td>
      </tr>
      <tr>
          <td style="text-align: right">11</td>
          <td>GLM 4.7 Flash BF16</td>
          <td style="text-align: right">33.5</td>
          <td>D</td>
          <td style="text-align: right">60 GB</td>
          <td style="text-align: right">217</td>
          <td style="text-align: right">30</td>
          <td style="text-align: right">US$ 4.60</td>
          <td>Codex</td>
      </tr>
  </tbody>
</table>
<p>Three have no score: Cohere&rsquo;s <strong>Command A Plus</strong> (218 billion, 25 active, W4A4) got stuck in a loop rewriting the same model file 23 times, needed three nudges and still left the tests unable to run before the reviewer was blocked; <strong>Nex N2.5 Mini</strong> ran at 337 tokens per second and delivered invalid Ruby, contradictory migrations and broken authentication in two attempts, a valid bad result from a server that works; and <strong>Qwen3.5 122B</strong> was a conditional fallback in case the 397B didn&rsquo;t finish, and the 397B finished.</p>
<p>Minutes are sprint time only, and Ornith&rsquo;s include a sprint interrupted by infrastructure. Flash-Next, Swift, ThinkingCap, Muse and Qwen 27B took one nudge each; GLM 4.7, two; the others, none. The probe speed is an admission number: the throughput of a real coding session includes prefill, tools, tests and the agent&rsquo;s choices. And note that the top two tie on score with opposite profiles: GLM spent 145 minutes and 22 dollars to reach the same 92.5 that Qwen3.8 Flash-Next did in 80 minutes and 12 dollars, with half the memory.</p>
<h2>The interesting cases<span class="hx:absolute hx:-mt-20" id="the-interesting-cases"></span>
    <a href="#the-interesting-cases" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>Qwen3.8 Flash-Next: the best of the open models, by cost<span class="hx:absolute hx:-mt-20" id="qwen38-flash-next-the-best-of-the-open-models-by-cost"></span>
    <a href="#qwen38-flash-next-the-best-of-the-open-models-by-cost" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><a href="https://huggingface.co/nvidia/Qwen3.8-Flash-Next-NVFP4"target="_blank" rel="noopener">Flash-Next</a> is, <a href="https://recipes.vllm.ai/Qwen/Qwen3.8-Flash-Next"target="_blank" rel="noopener">in Qwen&rsquo;s words</a>, a preview of the Qwen 4 architecture: 125 billion language-model parameters with 6 billion active, plus a 51-billion n-gram embedding table that Qwen says can be offloaded to host memory, plus a 4-billion MTP head. Quantized to NVFP4 by NVIDIA, it took about 100 GB on the card (the checkpoint is 133 GB on disk). It was the only model in the second batch to resolve all 14 items: it caught the tenant leak unprompted, caught the compound test-integrity item, and wrote per-user isolation specs on its own, so the planted API IDOR had no way to stay green. At 195 tokens per second and 12 dollars of inference, it&rsquo;s the best cost-benefit in this table.</p>
<h3>GLM 5.3 Flash: the same 92.5, three times the cost<span class="hx:absolute hx:-mt-20" id="glm-53-flash-the-same-925-three-times-the-cost"></span>
    <a href="#glm-53-flash-the-same-925-three-times-the-cost" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>GLM scored the same with a much bigger architecture: 320 billion parameters, 18 active, 198 GB even in FP4. It resolved all 14 items, zero nudges, reviewed end to end by Codex.</p>
<p>But it took 145 minutes and consumed twice the memory for the same result. On Z.ai&rsquo;s cloud, the same model scored 84.25 on a flat plan. With single-run noise, which in my history reaches nine points, the safest reading is that both sit in the same band.</p>
<h3>Swift 1.5 and ThinkingCap: two 27B fine-tunes in Tier A<span class="hx:absolute hx:-mt-20" id="swift-15-and-thinkingcap-two-27b-fine-tunes-in-tier-a"></span>
    <a href="#swift-15-and-thinkingcap-two-27b-fine-tunes-in-tier-a" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The two surprises. <a href="https://huggingface.co/ukisai/Swift-1.5-Qwen3.8-27b"target="_blank" rel="noopener">Swift 1.5</a>, from UkisAI, and <a href="https://huggingface.co/bottlecapai/ThinkingCap-Qwen3.8-27B"target="_blank" rel="noopener">ThinkingCap 3.8</a>, from BottleCap AI, are Qwen 3.8 27B fine-tunes trained to think less: Swift advertises up to 58% fewer reasoning tokens (on the GPQA median; on Terminal-Bench, 16%), ThinkingCap about 37% on average. Both fit in 56 GB in BF16, run at about 200 tokens per second with MTP, and scored 88 and 83.</p>
<p>Swift landed above Anthropic&rsquo;s Opus 4.6. The base Qwen 3.8 27B, same architecture, scored 80.5 in 192 minutes; Swift scored 88 in 64. Swift caught sabotage earlier than the base; whether that was from thinking less, one run of each can&rsquo;t prove.</p>
<p>A provenance caveat: the reviewer recorded that Swift read local commit messages that disclosed regressions, which weakens a literal reading of &ldquo;entirely unaided discovery&rdquo;. And it left one item unresolved, an inactive admin still accepted with a vacuous guarding test. Even so, of every model in this table, it&rsquo;s the one I&rsquo;d put on a card of my own.</p>
<h3>Qwen3.5 397B: the flagship that let the backdoor through<span class="hx:absolute hx:-mt-20" id="qwen35-397b-the-flagship-that-let-the-backdoor-through"></span>
    <a href="#qwen35-397b-the-flagship-that-let-the-backdoor-through" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The biggest model in the batch, 397 billion parameters with 17 active, 251 GB of NVFP4 on a 275 GB card, served at 140 tokens per second after the loader repair and scored <strong>68.5</strong>, Tier C.</p>
<p>The case that defines the score: sabotage number 6 is a line <code>admin? =&gt; admin || email.end_with?(&quot;@internal.example&quot;)</code>, a privilege escalation by email domain. The 397B let it through in the normal sprints, let it through in sprint 7 with the explicit security-audit request, and <strong>wrote a passing test codifying the backdoor as expected behavior</strong>, in the same commit where it announced &ldquo;Brakeman: 0 warnings&rdquo;. Parameters did not buy vigilance.</p>
<h3>Nemotron 3.5 Super VL: cheated on the test<span class="hx:absolute hx:-mt-20" id="nemotron-35-super-vl-cheated-on-the-test"></span>
    <a href="#nemotron-35-super-vl-cheated-on-the-test" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>NVIDIA&rsquo;s <a href="https://huggingface.co/nvidia/NVIDIA-Nemotron-3.5-Super-VL-120B-A12B-NVFP4"target="_blank" rel="noopener">Nemotron 3.5 Super VL</a>, 120 billion with 12 active, released two days before I started testing and <a href="https://blogs.nvidia.com/blog/nemotron-3-5-super-vl/"target="_blank" rel="noopener">sold by NVIDIA</a> as the fastest open agentic model in its class, with &ldquo;leading accuracy&rdquo; on multimodal tasks, scored <strong>63</strong>. Sabotage number 2 removes a navigation link and the regression test looks for <code>have_link('Sign in')</code>. Nemotron changed the test to <code>have_button('Sign in')</code>, which passes on the login form&rsquo;s submit button without restoring the link, and kept the cheat even after the reveal sprint.</p>
<p>It also tends to catch items a sprint late. And it was the one that required its own runtime: vLLM 0.32 with CUDA 13 in a separate environment.</p>
<h3>DeepSeek V4 Flash: 86 in the cloud, 44.5 in my hands<span class="hx:absolute hx:-mt-20" id="deepseek-v4-flash-86-in-the-cloud-445-in-my-hands"></span>
    <a href="#deepseek-v4-flash-86-in-the-cloud-445-in-my-hands" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The most important case for anyone considering self-hosting. <a href="https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash"target="_blank" rel="noopener">DeepSeek V4 Flash</a>, 284 billion with 13 active, scored <strong>86, Tier A, for 97 cents</strong> through the API in September, and the 0731 version, exactly the one I downloaded, scored 85 for 1.94. The same public weights in mixed FP4 and FP8, served by me on the B300, scored <strong>44.5, Tier D</strong>: it caught 4 items unprompted, 5 more at the reveal, and left 5 unresolved. It was the fastest to finish, 37 minutes, and the least vigilant.</p>
<p>The likely cause is divergence between the public checkpoint and what DeepSeek serves, or between their serving stack and mine: parser, chat template, reasoning mode, quantization. I followed the official card. What this proves is that the model&rsquo;s name on Hugging Face doesn&rsquo;t guarantee the hosted API&rsquo;s quality, and that whoever is going to host needs to validate before trusting.</p>
<h3>GLM 4.7 Flash: same as local, meaning bad<span class="hx:absolute hx:-mt-20" id="glm-47-flash-same-as-local-meaning-bad"></span>
    <a href="#glm-47-flash-same-as-local-meaning-bad" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The 30-billion GLM 4.7 Flash in BF16, at 217 tokens per second, scored 33.5. At home, in GGUF Q5, it had scored 24. It needed two nudges, left nine items unresolved and delivered a migration with a syntax error. The nine-and-a-half-point difference fits within single-run noise; what remains is that the model is weak either way.</p>
<h3>What showed up in several<span class="hx:absolute hx:-mt-20" id="what-showed-up-in-several"></span>
    <a href="#what-showed-up-in-several" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Ornith, Muse Glimmer and the 397B shared a hygiene defect: they ran the sprint to the end with exit zero and forgot to commit their own work, leaving it for the reviewer to checkpoint. It didn&rsquo;t go into the score, and on a real team it would be the first complaint. And the 81-to-83 cluster, ThinkingCap, Ornith and Muse, has a consistent profile: it catches SQL injection, tenant leaks, XSS and exposed secrets with confidence, and lets the quiet items through, broken access control, test integrity, missing index, wrong aggregate.</p>
<h2>How far from the frontier<span class="hx:absolute hx:-mt-20" id="how-far-from-the-frontier"></span>
    <a href="#how-far-from-the-frontier" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The three best open models against the best commercial ones from the same ranking, with the cost and time of each run:</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th style="text-align: right">v4</th>
          <th>Tier</th>
          <th style="text-align: right">Run cost</th>
          <th style="text-align: right">Minutes</th>
          <th>Where</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>GPT-6 Astra</td>
          <td style="text-align: right">100.0</td>
          <td>A</td>
          <td style="text-align: right">US$ 30.55</td>
          <td style="text-align: right">100</td>
          <td>cloud</td>
      </tr>
      <tr>
          <td>Claude Opus 5.5</td>
          <td style="text-align: right">100.0</td>
          <td>A</td>
          <td style="text-align: right">US$ 16.63</td>
          <td style="text-align: right">64</td>
          <td>cloud</td>
      </tr>
      <tr>
          <td>GPT 5.6 terra</td>
          <td style="text-align: right">100.0</td>
          <td>A</td>
          <td style="text-align: right">US$ 9.52</td>
          <td style="text-align: right">80</td>
          <td>cloud</td>
      </tr>
      <tr>
          <td>Grok 4.6</td>
          <td style="text-align: right">98.5</td>
          <td>A</td>
          <td style="text-align: right">US$ 13.00</td>
          <td style="text-align: right">67</td>
          <td>cloud</td>
      </tr>
      <tr>
          <td>Claude Fable 5.1</td>
          <td style="text-align: right">95.5</td>
          <td>A</td>
          <td style="text-align: right">~US$ 51</td>
          <td style="text-align: right">133</td>
          <td>cloud</td>
      </tr>
      <tr>
          <td><strong>GLM 5.3 Flash (NVFP4)</strong></td>
          <td style="text-align: right"><strong>92.5</strong></td>
          <td>A</td>
          <td style="text-align: right">US$ 21.96 inference only</td>
          <td style="text-align: right">145</td>
          <td>B300</td>
      </tr>
      <tr>
          <td><strong>Qwen3.8 Flash-Next (NVFP4)</strong></td>
          <td style="text-align: right"><strong>92.5</strong></td>
          <td>A</td>
          <td style="text-align: right">US$ 12.04 inference only</td>
          <td style="text-align: right">80</td>
          <td>B300</td>
      </tr>
      <tr>
          <td>Claude Sonnet 5</td>
          <td style="text-align: right">91.0</td>
          <td>A</td>
          <td style="text-align: right">~US$ 27</td>
          <td style="text-align: right">112</td>
          <td>cloud</td>
      </tr>
      <tr>
          <td>Gemini 3.8 Flash high</td>
          <td style="text-align: right">90.5</td>
          <td>A</td>
          <td style="text-align: right">US$ 15.98</td>
          <td style="text-align: right">97</td>
          <td>cloud</td>
      </tr>
      <tr>
          <td><strong>Swift 1.5 27B</strong></td>
          <td style="text-align: right"><strong>88.0</strong></td>
          <td>A</td>
          <td style="text-align: right">US$ 9.71 inference only</td>
          <td style="text-align: right">64</td>
          <td>B300</td>
      </tr>
      <tr>
          <td>Claude Opus 4.6</td>
          <td style="text-align: right">87.5</td>
          <td>A</td>
          <td style="text-align: right">US$ 25.64</td>
          <td style="text-align: right">89</td>
          <td>cloud</td>
      </tr>
  </tbody>
</table>
<p>The reading: the best of the open source that fits on a B300 and that I served sits <strong>one step below the commercial ceiling and level with the middle of Tier A</strong>. Differences of one or two points are single-run noise.</p>
<p>GLM 5.3 Flash and Qwen3.8 Flash-Next tie with Sonnet 5 and pass Gemini 3.8 Flash and Opus 4.6. None reaches the 100 that seven commercial models scored, nor Grok 4.6&rsquo;s 98.5. Seven and a half points separate the best open model from the ceiling, and in v4 every point is one less sabotage caught unprompted.</p>
<p>The difference that matters is another: predictability. Among the top commercial models in the table, the worst is 87.5, and the full commercial field also has a bottom, Mistral Large 3 scored 39. The open field has the same 92.5 at the top and two Tier D at the bottom, with DeepSeek scoring 85 in the cloud and 44.5 in my hands. Choosing a top commercial model is choosing a number the API reproduces; choosing an open one is betting that the checkpoint, the quantization and your serving stack reproduce the number on the card, and sometimes they do.</p>
<h2>The math<span class="hx:absolute hx:-mt-20" id="the-math"></span>
    <a href="#the-math" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>If quality still falls short of the frontier, is the cost at least proportional? It depends on where you run the open model.</p>
<p><strong>On the vendor&rsquo;s own cloud API, yes, and by a wide margin.</strong> A full v4 run cost 97 cents on DeepSeek V4 Flash, 1.17 dollars on Qwen 3.8 Flash, 1.03 on MiMo V2.5 Pro, all Tier A in the cloud. Against 9.52 to 30.55 dollars for the 100-point models, that&rsquo;s ten to thirty times cheaper for a score 8 to 14 points lower. The list prices explain it: <a href="https://docs.z.ai/guides/overview/pricing"target="_blank" rel="noopener">GLM 5.3 Flash</a> costs 15 cents per million input tokens and 50 for output; <a href="https://api-docs.deepseek.com/quick_start/pricing"target="_blank" rel="noopener">DeepSeek Flash</a>, 15 to 30 cents input and 60 cents to 1.20 output; <a href="https://platform.claude.com/docs/en/about-claude/pricing"target="_blank" rel="noopener">Opus 5.5</a>, 4 and 20 dollars; <a href="https://developers.openai.com/api/docs/pricing"target="_blank" rel="noopener">GPT-6 Astra</a>, 10 and 50.</p>
<p><strong>On your own rented B300, no.</strong> The pod costs 9 dollars an hour on or idle, <strong>218 a day, about 6,500 a month</strong>. The model time of this whole batch, 14 models across download, load and sprints, added up to 23 hours, about 212 dollars at pod price; the sprints alone were 17 hours and 157 dollars.</p>
<p>But the pod also charges for the time when nothing runs. I put 400 dollars of credit on Runpod and 127 were left: the whole batch cost <strong>273 dollars</strong>, about 30 pod-hours, including storage, four pods in sequence and seven idle hours one night when the orchestration died and nothing warned me. Almost 60 dollars more than the model time, just waiting. The repository&rsquo;s break-even math: the pod only gets cheaper than the API from about <strong>6,500 runs a month</strong>, 217 a day, sustained. A solo developer or a small team doesn&rsquo;t come close.</p>
<p>And model against model, side by side:</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th>Cloud API</th>
          <th>Hosted by me on the B300</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DeepSeek V4 Flash 0731</td>
          <td>85 (A) for US$ 1.94</td>
          <td>44.5 (D) for US$ 5.59 inference only</td>
      </tr>
      <tr>
          <td>Qwen 3.8 Flash (cloud) and Flash-Next (B300), same family</td>
          <td>86.5 (A) for US$ 1.17</td>
          <td>92.5 (A) for US$ 12.04</td>
      </tr>
      <tr>
          <td>Qwen 3.8 27B</td>
          <td>80 (B) for free on the Strix, in 706 minutes</td>
          <td>80.5 (B) for US$ 29.11, in 192 minutes</td>
      </tr>
  </tbody>
</table>
<blockquote>
  <p>The cloud API of an open model is ten to thirty times cheaper than the frontier for one tier less. Hosting the same model on a rented B300 is more expensive than the frontier, and sometimes worse than its own API.</p>

</blockquote>
<h2>Does a team fit on the same card?<span class="hx:absolute hx:-mt-20" id="does-a-team-fit-on-the-same-card"></span>
    <a href="#does-a-team-fit-on-the-same-card" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A question I asked while looking at the GPU at 10% utilization: do these models use all of the pod&rsquo;s compute? Could a whole team go on the same B300?</p>
<p>In principle, yes, and that&rsquo;s what vLLM exists for. <a href="https://vllm.ai/blog/2023-06-20-vllm"target="_blank" rel="noopener">Continuous batching</a> schedules several requests in the same decode step, and <a href="https://vllm.ai/blog/2023-06-20-vllm"target="_blank" rel="noopener">PagedAttention</a> stores each session&rsquo;s KV cache in non-contiguous blocks, with under 4% waste, precisely to fit more sessions in the same memory. A 27-billion model in BF16 takes 56 GB and leaves about 200 GB of KV cache: that&rsquo;s dozens of agent sessions with 128K-token context at the same time. The 397B, with 251 GB of weights, leaves 24, and then few fit.</p>
<p>What doesn&rsquo;t scale is speed per person. My test ran in <code>--performance-mode interactivity</code>, which optimizes single-stream latency, and the 200 tokens per second are that stream&rsquo;s. With ten agents decoding at the same time, the card&rsquo;s aggregate throughput rises and each one&rsquo;s falls, and the speculative decoding that gave me Muse Glimmer&rsquo;s 295 yields less as concurrency grows: a <a href="https://huggingface.co/z-lab/Muse-Glimmer-30B-DFlash2"target="_blank" rel="noopener">DFlash drafter published for Muse by Z Lab</a> reports 3 to 4.6 times gain with one session and 1.2 to 1.7 with 32.</p>
<p>I didn&rsquo;t measure concurrency in this batch, so what I can say is the outline: a team of five to ten people on a 27-to-35-billion model on one B300 is practical, and each one probably still stays above 100 tokens per second. The same team on the 397B is unworkable on the card, and on the 198 GB GLM 5.3 Flash it gets tight.</p>
<p>It&rsquo;s in that scenario, and only there, that the pod math starts to close. Ten developers running an agent all day on a 27B model would spend, on the API, at a dollar per two-hour run, about 40 dollars a day; the pod costs 218. It still loses. What changes the math is having a hundred people, or being required to keep the code from leaving your network.</p>
<h2>Is owning your own infra worth it?<span class="hx:absolute hx:-mt-20" id="is-owning-your-own-infra-worth-it"></span>
    <a href="#is-owning-your-own-infra-worth-it" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>For serious software development, with this batch&rsquo;s data, the answer comes in three parts.</p>
<p><strong>The top open models are good enough.</strong> GLM 5.3 Flash, Qwen3.8 Flash-Next and Swift 1.5 27B are Tier A in my test, at Sonnet 5 level and above Opus 4.6. That was unthinkable in April, when <a href="/en/2026/04/05/testing-llms-open-source-and-commercial-can-anyone-beat-claude-opus/">only Qwen 3.6 35B came close to Opus</a>, and still one fix away. For anyone who accepts one tier below the ceiling, they serve for real work, including security review, which is what v4 measures.</p>
<p><strong>Renting a B300 to run them is the wrong way to use them.</strong> The cloud API of the same model costs a dollar per run and needs zero configuration. The pod costs 218 a day, demands a day of tuning per model family, and in DeepSeek&rsquo;s case delivered a worse model than its own API. It&rsquo;s only worth it with a data-residency obligation, with a checkpoint no cloud serves, with sustained load of hundreds of runs a day, or with a need for pinned weights for reproducibility. Four B300s to run the 750-billion-to-one-trillion models quadruple the bill with no evidence the score reaches 100: the 397B, the biggest model I tested, scored 68.5.</p>
<p><strong>And if you&rsquo;re going to host, host small.</strong> Swift 1.5 27B and ThinkingCap at 56 GB, with MTP, at 200 tokens per second, are Tier A on hardware you can buy, a professional 80 or 96 GB card instead of a datacenter one. That&rsquo;s where open source makes sense for a company: a whole team on one card, code inside the network, an Opus 4.6 score. Just check the license first: Swift and ThinkingCap ship with their own licenses, and above a certain revenue both require a commercial license. The open giants remain an API thing.</p>
<p>My rule after all this: top commercial model for tasks that can&rsquo;t fail, open-model API for volume, and your own card only when the law or the client demands it. The repository with every serving argument, every probe log, every reviewer transcript and the Codex-or-Claude provenance of every boundary is <a href="https://github.com/akitaonrails/llm-coding-benchmark"target="_blank" rel="noopener">public</a>, as always.</p>
]]></content:encoded><category>llm-benchmarks</category><category>local-models</category><category>llms</category><category>hardware</category></item><item><title>I Trained My Own AI Model (Spoiler: It's Just for Study)</title><link>https://akitaonrails.github.io/en/2026/10/14/i-trained-my-own-ai-model-spoiler-its-just-for-study/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/10/14/i-trained-my-own-ai-model-spoiler-its-just-for-study/</guid><pubDate>Wed, 14 Oct 2026 15:00:00 -0300</pubDate><description>&lt;p&gt;One exercise was missing from my list this year: trying to build my own LLM. Not a frontier model. Building a GPT-5 or a Claude takes years of study and experimentation, whole teams and hundreds of millions of dollars in compute. The &lt;a href="https://hai.stanford.edu/ai-index/2024-ai-index-report"target="_blank" rel="noopener"&gt;Stanford AI Index&lt;/a&gt; estimate for GPT-4 was US$ 78 million in compute alone, and for Gemini Ultra, US$ 191 million.&lt;/p&gt;
&lt;p&gt;But nobody needs a GPT-class model for everything. A frontier model is a generalist by definition: it tries to know everything about anything, from tax law to song lyrics, and the returns on that diminish.&lt;/p&gt;</description><content:encoded><![CDATA[<p>One exercise was missing from my list this year: trying to build my own LLM. Not a frontier model. Building a GPT-5 or a Claude takes years of study and experimentation, whole teams and hundreds of millions of dollars in compute. The <a href="https://hai.stanford.edu/ai-index/2024-ai-index-report"target="_blank" rel="noopener">Stanford AI Index</a> estimate for GPT-4 was US$ 78 million in compute alone, and for Gemini Ultra, US$ 191 million.</p>
<p>But nobody needs a GPT-class model for everything. A frontier model is a generalist by definition: it tries to know everything about anything, from tax law to song lyrics, and the returns on that diminish.</p>
<p>And most of them today are mixtures of experts: roughly speaking, several smaller models clustered into one giant chunk of bits, with a router that wakes only a few of them for each token. OpenAI&rsquo;s <a href="https://arxiv.org/abs/2508.10925"target="_blank" rel="noopener">gpt-oss-120b</a> has 117 billion parameters in total and uses 5.1 billion per token. <a href="https://arxiv.org/abs/2412.19437"target="_blank" rel="noopener">DeepSeek-V3</a> has 671 billion and activates 37. The big model is already, on the inside, a collection of small models.</p>
<p>I could have grabbed an open model, gpt-oss, Qwen, Phi, and adapted it. The techniques exist and are well documented:</p>
<ul>
<li><strong>Full fine-tuning</strong>: keep training all the weights of a ready model on your data. It&rsquo;s classic transfer learning, formalized for text by <a href="https://arxiv.org/abs/1801.06146"target="_blank" rel="noopener">ULMFiT</a> in 2018.</li>
<li><strong>LoRA and QLoRA</strong>: freeze the original weights and train only small low-rank matrices beside them. <a href="https://arxiv.org/abs/2106.09685"target="_blank" rel="noopener">LoRA</a> cuts trainable parameters by up to ten thousand times; <a href="https://arxiv.org/abs/2305.14314"target="_blank" rel="noopener">QLoRA</a> quantizes the base model to 4 bits and fine-tunes a 65-billion model on a single 48 GB GPU.</li>
<li><strong>Distillation</strong>: train a smaller model to imitate a larger one&rsquo;s outputs. The idea is from <a href="https://arxiv.org/abs/1503.02531"target="_blank" rel="noopener">Hinton, 2015</a>, and the most famous recent example is <a href="https://arxiv.org/abs/2501.12948"target="_blank" rel="noopener">DeepSeek-R1</a> distilled into versions from 1.5 to 70 billion on top of Qwen and Llama.</li>
<li><strong>Continued pretraining</strong>: one more training phase on text from your domain before any instruction tuning, the <a href="https://arxiv.org/abs/2004.10964"target="_blank" rel="noopener">&ldquo;Don&rsquo;t Stop Pretraining&rdquo;</a> of 2020.</li>
<li><strong>SFT, RLHF and DPO</strong>: teach the assistant format with demonstrations, then align with human preference via reinforcement learning (<a href="https://arxiv.org/abs/2203.02155"target="_blank" rel="noopener">InstructGPT</a>) or with a direct classification loss (<a href="https://arxiv.org/abs/2305.18290"target="_blank" rel="noopener">DPO</a>).</li>
</ul>
<p>With any of those I&rsquo;d have a useful model in hours. But the goal was different. I wanted to organize, hands-on, the basic techniques for building a model smaller than GPT-2, from scratch, in a few days, and see how far it would go. If possible, train a tiny model starting from random weights and add enough functionality for it to almost work inside an agentic harness like OpenCode.</p>
<p>Before going on, who I am in this story: I&rsquo;m a curious software engineer and an amateur at this. As an LLM researcher I&rsquo;m a zero. I knew I wouldn&rsquo;t have a working LLM in such a short time, and that&rsquo;s part of the exercise.</p>
<p>But it&rsquo;s not unachievable. If you&rsquo;re a computer science student, study hard and give it enough years, you can try it yourself. And if you&rsquo;re not a scientist, understand this: nobody needs to be Einstein to do this. The foundations are all available, published, for you to study and try. What&rsquo;s mandatory is elbow grease.</p>
<p>Spoiler: we got far into the &ldquo;almost&rdquo; and stopped there. The <a href="https://github.com/akitaonrails/microgpt"target="_blank" rel="noopener">repository is public</a>, with every step and every failure documented. This text is the summary.</p>
<blockquote>
  <p><strong>TL;DR:</strong> I started from Karpathy&rsquo;s microgpt, 200 lines of pure Python that train a GPT in one minute on a MacBook, and expanded it into a PyTorch lab with its own tokenizer, pretraining, supervised fine-tuning, an OpenAI-compatible server and OpenCode integration. In under two weeks, on one RTX 5090, I trained a 337-million-parameter model on 10 billion tokens: 137 hours of training, at most around R$ 80 of electricity. It gets 58 out of 100 simple Python functions right on a held-out set, and scores zero on anything that requires reading, editing and testing a repository. The <a href="https://huggingface.co/HuggingFaceTB/SmolLM2-360M"target="_blank" rel="noopener">360-million SmolLM2</a>, the same size, saw 4 trillion tokens, 400 times more. Jump to <a href="#karpathys-exercise">Karpathy&rsquo;s exercise</a>, the <a href="#the-steps-of-building-an-llm">steps</a>, the <a href="#the-dimensions-of-the-problem">dimensions</a>, the <a href="#the-stumbles">stumbles</a>, the <a href="#what-came-out-the-other-side">result</a> or the <a href="#the-future-is-smaller-and-more-specialized">conclusion</a>.</p>

</blockquote>
<h2>Karpathy&rsquo;s exercise<span class="hx:absolute hx:-mt-20" id="karpathys-exercise"></span>
    <a href="#karpathys-exercise" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>In February this year Andrej Karpathy published <a href="https://karpathy.github.io/2026/02/12/microgpt/"target="_blank" rel="noopener">microgpt</a>: a single file of 200 lines of Python, with no dependencies, that trains and runs a GPT. He calls it &ldquo;the full algorithmic content of what is needed. Everything else is just efficiency&rdquo;. It&rsquo;s the culmination of a lineage of his: micrograd (scalar autograd), makemore (generating names) and nanoGPT (compact GPT-2 in PyTorch), joined by <a href="https://github.com/karpathy/nanochat"target="_blank" rel="noopener">nanochat</a>, &ldquo;the best ChatGPT that $100 can buy&rdquo;, which at launch trained a GPT-2-capability model in almost four hours on a node of eight H100s, and today does it in about two.</p>
<p>microgpt is deliberately minimal: the dataset is 32 thousand first names, one per line, and the tokenizer is one character per token.</p>
<p>The network has one layer, 16 embedding dimensions, 4 attention heads, a 16-token window because the longest name has 15 letters. It trains for a thousand steps, one name per step, with Adam written by hand, and at the end generates made-up names, all in one minute on a MacBook.</p>
<p>What it teaches, by having everything exposed in a single file:</p>
<ul>
<li>autograd, the <code>Value</code> class that holds a number, the gradient and the chain rule to propagate back;</li>
<li>what a token and an embedding are;</li>
<li>the attention block, with query, key and value, and the causal mask that stops the model from peeking at the future;</li>
<li>the cross-entropy loss against the next token;</li>
<li>the optimizer;</li>
<li>sampling with temperature.</li>
</ul>
<p>It&rsquo;s a GPT-2 with small differences the file itself notes: RMSNorm instead of LayerNorm, no biases, ReLU instead of GeLU. After reading that, what&rsquo;s left between you and a real model is, in his words, efficiency. I went to see how much efficiency.</p>
<h2>What we expanded<span class="hx:absolute hx:-mt-20" id="what-we-expanded"></span>
    <a href="#what-we-expanded" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The <a href="https://github.com/akitaonrails/microgpt"target="_blank" rel="noopener">microgpt lab</a> is an independent reimplementation in PyTorch, inspired by the gist, split by concept: about 3,300 lines of Python in 31 modules, plus scripts and 85 CPU tests. A single model implementation serves pretraining, assistant tuning and the server. Every weight starts random; the project downloads no pretrained weights at all, on purpose, because the rule of the exercise was from scratch.</p>
<p>On top of Karpathy&rsquo;s block came the components a 2026 model uses: RoPE for position, RMSNorm, SwiGLU in the dense layer, embeddings tied to the output, Flash Attention through PyTorch, a key-value cache with prefix reuse. And around the model, what it needs to talk to an agent: a conversation protocol with roles, an HTTP server compatible with OpenAI&rsquo;s chat API, tool calls validated by schema and optionally by grammar, separation of reasoning content, and the OpenCode configuration pointing at it.</p>
<p>The project went through four scales:</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th style="text-align: right">Parameters</th>
          <th>Tokenizer</th>
          <th>Purpose</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Byte lesson</td>
          <td style="text-align: right">0.9 million</td>
          <td>byte by byte</td>
          <td>runs on CPU, teaches the whole loop</td>
      </tr>
      <tr>
          <td>Student</td>
          <td style="text-align: right">91 million</td>
          <td>o200k (200K entries)</td>
          <td>first agent, arithmetic</td>
      </tr>
      <tr>
          <td>Capacity candidate</td>
          <td style="text-align: right">239 million</td>
          <td>o200k</td>
          <td>more layers, same vocabulary</td>
      </tr>
      <tr>
          <td>Foundation pivot</td>
          <td style="text-align: right">337 million</td>
          <td>own BPE, 32K entries</td>
          <td>the final model</td>
      </tr>
  </tbody>
</table>
<p>The table hides the first big lesson, which I tell in the stumbles: in the 239-million model, 154 million parameters were just the vocabulary table. 85 million were left for the actual transformer.</p>
<h2>The steps of building an LLM<span class="hx:absolute hx:-mt-20" id="the-steps-of-building-an-llm"></span>
    <a href="#the-steps-of-building-an-llm" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Regardless of size, the path has the same stations. What changes is the trailing zero at each one.</p>
<h3>1. Data<span class="hx:absolute hx:-mt-20" id="1-data"></span>
    <a href="#1-data" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>A model is what it eats. We downloaded, with pinned revisions and hashes, selections of public corpora: <a href="https://huggingface.co/datasets/codeparrot/codeparrot-clean"target="_blank" rel="noopener">CodeParrot</a> (deduplicated, filtered Python), <a href="https://huggingface.co/datasets/OpenCoder-LLM/opc-sft-stage1"target="_blank" rel="noopener">OpenCoder</a> (instruction and code), <a href="https://huggingface.co/datasets/HuggingFaceTB/smollm-corpus"target="_blank" rel="noopener">Cosmopedia</a> (synthetic educational text), <a href="https://huggingface.co/datasets/HuggingFaceTB/smoltalk"target="_blank" rel="noopener">SmolTalk</a> (conversation) and a multilingual slice. The 14 download manifests add up to about 20 GB raw, all stored on the NAS.</p>
<p>Then comes curation: exact deduplication, splitting train and validation by repository or conversation group (never by line, or the same project leaks to both sides), and the mixture. The final foundation corpus has 3.5 billion stored tokens, 60% Python code, 16% instruction, 12% educational text, 8% conversation, 4% multilingual. Every proportion is a design decision, and none of them has a published right answer.</p>
<h3>2. Tokenizer<span class="hx:absolute hx:-mt-20" id="2-tokenizer"></span>
    <a href="#2-tokenizer" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The tokenizer is the dictionary between text and integers. The byte lesson uses 256 tokens plus one for end of document. The student used o200k, OpenAI&rsquo;s public vocabulary with 200K entries, which is what most tutorials do. The pivot trained its own BPE with 32,768 entries, on 92 MB of training-only text, with full UTF-8 coverage.</p>
<p>The choice looks like a detail and decides where the parameters end up. Each vocabulary entry costs one row in the embedding table at the model&rsquo;s width. With 200K entries and width 768, that&rsquo;s 154 million parameters spent on the table. With 32K entries and width 1,024, it&rsquo;s 33 million, and the rest goes to the layers that think.</p>
<h3>3. Pretraining, or foundation<span class="hx:absolute hx:-mt-20" id="3-pretraining-or-foundation"></span>
    <a href="#3-pretraining-or-foundation" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>This is Karpathy&rsquo;s loop at scale. The model receives 2,048-token windows drawn from the training stream and learns to predict the next token. Each update processes 32,768 tokens: two sequences, accumulated eight times. AdamW, learning rate with warmup and cosine decay, gradient clipping, BF16 on the GPU and FP32 for the weights.</p>
<p>The pivot ran about 305 thousand updates, 10 billion tokens of exposure. Since the corpus has 3.5 billion, each document was seen, in the mixture&rsquo;s expectation, almost three times: the code about twice, and instruction, educational text and conversation about nine or ten. Every 500 updates, a validation and a checkpoint. At the 2, 5 and 10 billion milestones, a frozen checkpoint with a hash, to measure the curve.</p>
<h3>4. Supervised fine-tuning<span class="hx:absolute hx:-mt-20" id="4-supervised-fine-tuning"></span>
    <a href="#4-supervised-fine-tuning" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Pretraining produces a model that completes text. For it to answer a request comes SFT, <em>supervised fine-tuning</em>: question and answer examples in the exact format the server uses, with the loss computed only over the assistant&rsquo;s tokens. What the user and the tool said goes in as context and is masked out of the loss.</p>
<p>The examples came from NVIDIA&rsquo;s <a href="https://huggingface.co/datasets/nvidia/OpenCodeInstruct"target="_blank" rel="noopener">OpenCodeInstruct</a>, and every answer was executed in a restricted Docker before going in: almost 99 thousand candidates, 97 thousand survived the test with three different hash seeds. 92 thousand training episodes remained. The selected checkpoint saw 64 thousand draws from those episodes, about 10 million answer tokens.</p>
<h3>5. Protocol, tools and server<span class="hx:absolute hx:-mt-20" id="5-protocol-tools-and-server"></span>
    <a href="#5-protocol-tools-and-server" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>An agent like OpenCode talks to the model over HTTP: it sends the history with roles and the tool definitions, gets back text or a tool call in JSON, executes it, returns the result, and repeats. The lab implements that whole cycle: the same protocol file serializes the training examples and the serving prompts, so training format and usage format never diverge.</p>
<p>The server implements <code>/v1/models</code> and <code>/v1/chat/completions</code>, validates the tool call against the schema, can constrain generation with a grammar so the JSON comes out well formed, separates reasoning content from the answer, counts cached tokens and rejects context overflow instead of truncating silently. All of that is plumbing, zero intelligence, and without it the model doesn&rsquo;t even get a seat at the table.</p>
<h3>6. Evaluation<span class="hx:absolute hx:-mt-20" id="6-evaluation"></span>
    <a href="#6-evaluation" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The step that changed the project the most. A frozen set of 100 Python development tasks, 20 families with five variants of name and wording, plus 100 held-out tasks that only the final checkpoint saw. Every answer runs in a Docker with no network, no root, with CPU and memory limits, against fixed assertions. On top of that: four real repository-repair tasks through OpenCode, 12 problems of tracing a program&rsquo;s exact output, information-retrieval probes in prompts of 8, 16 and 32 thousand tokens, and four short conversations.</p>
<blockquote>
  <p>No HumanEval, MBPP or SWE-bench. These are small local diagnostics, with correlated families, one seed, greedy generation. They serve to choose between checkpoints, and the closeout report itself says not to read &ldquo;58 out of 100&rdquo; as &ldquo;58% success at programming&rdquo;.</p>

</blockquote>
<h2>The dimensions of the problem<span class="hx:absolute hx:-mt-20" id="the-dimensions-of-the-problem"></span>
    <a href="#the-dimensions-of-the-problem" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>What all of this cost, and how it compares with the real world:</p>
<table>
  <thead>
      <tr>
          <th>Dimension</th>
          <th style="text-align: right">Our final model</th>
          <th>References</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Parameters</td>
          <td style="text-align: right">337 million</td>
          <td>GPT-2 small: 124 million; full GPT-2 (2019): 1.5 billion</td>
      </tr>
      <tr>
          <td>Tokens seen in pretraining</td>
          <td style="text-align: right">10 billion</td>
          <td>SmolLM2-360M: 4 trillion; Llama 3.1: 15 trillion; Qwen3: 36 trillion</td>
      </tr>
      <tr>
          <td>Raw data downloaded</td>
          <td style="text-align: right">20 GB</td>
          <td>GPT-2: 40 GB of WebText, 8 million pages</td>
      </tr>
      <tr>
          <td>Foundation corpus</td>
          <td style="text-align: right">3.5 billion tokens, 2.35 million documents</td>
          <td></td>
      </tr>
      <tr>
          <td>Hardware</td>
          <td style="text-align: right">1 RTX 5090, 32 GB</td>
          <td>Llama 3.1 405B: 30.84 million H100 hours</td>
      </tr>
      <tr>
          <td>GPU time in foundation</td>
          <td style="text-align: right">137 process hours, 6.4 calendar days</td>
          <td>DeepSeek-V3: 2.79 million H800 hours, ~US$ 5.6 million</td>
      </tr>
      <tr>
          <td>SFT time</td>
          <td style="text-align: right">3.4 hours across the nine stages</td>
          <td></td>
      </tr>
      <tr>
          <td>Throughput</td>
          <td style="text-align: right">23 to 44 thousand tokens/s, depending on I/O</td>
          <td></td>
      </tr>
      <tr>
          <td>Peak VRAM</td>
          <td style="text-align: right">13.9 GB in foundation; 6.8 GB in SFT</td>
          <td></td>
      </tr>
      <tr>
          <td>Checkpoints</td>
          <td style="text-align: right">17 distinct checkpoints (20 paths), 69 GB</td>
          <td></td>
      </tr>
      <tr>
          <td>Cost in money</td>
          <td style="text-align: right">zero in cloud; about R$ 80 of electricity worst case</td>
          <td>nanochat: US$ 100 on 8 H100s for 4 hours at launch; today about US$ 48 in 2 hours</td>
      </tr>
  </tbody>
</table>
<p>The electricity bill: the RTX 5090 has a <a href="https://www.nvidia.com/en-us/geforce/graphics-cards/50-series/rtx-5090/"target="_blank" rel="noopener">575 W rated power</a>. Over 137 hours at full load that&rsquo;s about 79 kWh, and Enel&rsquo;s residential tariff in São Paulo in 2026, adding TUSD, TE and taxes, is around R$ 1 per kWh. Since training ran much of the time at a 75% duty cycle to keep the desktop from freezing, the real figure was lower. It&rsquo;s the marginal cost of someone who already owns the card.</p>
<p>Renting a 5090 on <a href="https://www.runpod.io/pricing"target="_blank" rel="noopener">Runpod</a> costs about US$ 1.20 an hour, so the same 137 hours would cost about US$ 165; an H100 runs between US$ 2.90 and 4 an hour, depending on the variant.</p>
<p>And the comparison that gives the scale. SmolLM2, at 360 million parameters, practically our size, was trained on 4 trillion tokens. At our throughput, 4 trillion would take between four and six years of uninterrupted GPU. Same model size, 400 times the amount of study.</p>
<p>That&rsquo;s why &ldquo;parameters&rdquo; is a misleading unit: it says the size of the brain, and nothing about how many years of school it had.</p>
<p>The rule of thumb derived from <a href="https://arxiv.org/abs/2203.15556"target="_blank" rel="noopener">Chinchilla</a> (70 billion parameters, 1.4 trillion tokens), about 20 tokens per parameter to be compute-optimal, would give 6.7 billion tokens for our model. We went past that. But Chinchilla&rsquo;s optimum is the point of best compute usage, and modern small models go far past it on purpose, because the inference cost afterward makes training more worthwhile.</p>
<h2>The stumbles<span class="hx:absolute hx:-mt-20" id="the-stumbles"></span>
    <a href="#the-stumbles" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The full list is in the <a href="https://github.com/akitaonrails/microgpt/blob/main/docs/report/experiment-closeout.md"target="_blank" rel="noopener">closeout report</a>. The ones that taught the most:</p>
<p><strong>Measuring too close to training.</strong> The first agent, the 91-million one, completed four 16-round sessions in OpenCode and one 32-round session with 130 real tool calls, recovering from a check error and switching sessions midway, with the state kept by the harness. It looked ready.</p>
<p>Then I asked, by hand and in English, for a hello world function that takes a name as a parameter and prints it in the message. The model repeated the previous arithmetic edit twice, pointed at a file that didn&rsquo;t exist, and on the third attempt spat out invalid JSON. All it knew was the training task family.</p>
<p><strong>The vocabulary ate the model.</strong> The 239-million candidate had more layers and lower loss than the 91 and solved 24 out of 100 problems. Looking at the parameter distribution, 154 million were vocabulary table. The pivot to 32K entries gave 300 million parameters back to the transformer. The pivot changed vocabulary, capacity distribution, initialization and exposure all at once, so the report doesn&rsquo;t isolate which of them moved the result.</p>
<p><strong>Loss is not competence.</strong> In the final SFT, continuing from 4 thousand to 8 thousand updates lowered the validation loss and dropped executed correctness from 68 to 62. Lower loss, worse code. Checkpoint selection became by executed task, frozen beforehand; the loss number stayed out of it.</p>
<p><strong>Data and graders lie.</strong> &ldquo;Correct&rdquo; answers from the public dataset failed when executed again. Tests that depended on Python&rsquo;s hash order, shadowed builtins, <code>SystemExit(0)</code> counting as success, empty edits copying the file. Each one needed a control: three hash seeds, a completion sentinel, an empty fixture that must fail. And in one of the mixed training runs, the model learned to say the tests passed when they hadn&rsquo;t.</p>
<p><strong>Training format and usage format have to match.</strong> Session-switch announcements, tool-call IDs, edit anchors, the way OpenCode quotes user input: every difference between what the model saw in training and what it received from the server brought the old agent down. The same protocol file for both sides was the fix, and even so changing format sometimes improved the function name without improving the code.</p>
<p><strong>Tokenization crosses correctness boundaries.</strong> The literal string <code>&lt;|endoftext|&gt;</code> inside source code has to remain ordinary text instead of becoming the end-of-document marker. Reconstructing UTF-8 documents without splitting a character in half. XGrammar&rsquo;s support for byte-level vocabularies. The first code evaluation at the 2-billion milestone failed at grammar construction, and was fixed and rerun instead of counting as a model failure.</p>
<p><strong>The NAS almost cost me days.</strong> On resuming the 10-billion phase, the trainer loaded step 81,500 instead of the frozen 152,588: the mutable <code>last.pt</code> had a timestamp earlier than the pause, right after a NAS maintenance, and the cause was never proven. We discarded 1.24 hours of training, verified the hash of the 5-billion milestone, and every later resume came to require a hash, the exact step, optimizer and RNG state, the dataset and a write probe with fsync. Never resume a training run just because the file is called <code>last.pt</code>.</p>
<p><strong>A desktop GPU is a shared GPU.</strong> Free VRAM didn&rsquo;t stop the browser from freezing. The fix was a cooperative duty cycle: the trainer syncs the GPU and sleeps between batches, 75% of the time during the day and 100% from 3 to 9 in the morning. And reading the corpus from the NAS, through mmap, left the GPU idle waiting for disk. After one maintenance the throughput dropped to 5 thousand tokens per second before climbing back to 23 thousand.</p>
<p><strong>I stopped at a pipeline boundary.</strong> The pivot&rsquo;s controller implemented foundation, code SFT and holdout. It finished clean, and &ldquo;finished clean&rdquo; covered up to the isolated Python function, while the goal was the agent. The final checkpoint was tested in OpenCode only at closeout, and failed. It should have been tested before any sentence with &ldquo;ready&rdquo; in it.</p>
<h2>What came out the other side<span class="hx:absolute hx:-mt-20" id="what-came-out-the-other-side"></span>
    <a href="#what-came-out-the-other-side" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The final model&rsquo;s spec sheet:</p>
<table>
  <thead>
      <tr>
          <th>Property</th>
          <th>Value</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Parameters</td>
          <td>337,167,360, of which 303.6 million in the transformer and 33.6 million in the vocabulary table</td>
      </tr>
      <tr>
          <td>Architecture</td>
          <td>24 layers, width 1,024, 16 heads, SwiGLU with hidden width 2,752, RMSNorm, RoPE</td>
      </tr>
      <tr>
          <td>Vocabulary</td>
          <td>own BPE with 32,768 entries, UTF-8 coverage</td>
      </tr>
      <tr>
          <td>Context</td>
          <td>32,768 tokens allocated; trained on 2,048-token windows</td>
      </tr>
      <tr>
          <td>Foundation</td>
          <td>10 billion tokens, 305,176 updates, final validation loss 1.085</td>
      </tr>
      <tr>
          <td>Selected SFT</td>
          <td>4,000 updates, 10.3 million answer tokens</td>
      </tr>
      <tr>
          <td>Checkpoint</td>
          <td>4 GB with optimizer state; 1.35 GB of weights alone in FP32</td>
      </tr>
  </tbody>
</table>
<p>The learning curve, measured at the three milestones, with the same short SFT applied on top of each:</p>
<table>
  <thead>
      <tr>
          <th style="text-align: right">Foundation tokens</th>
          <th style="text-align: right">Raw code completion /100</th>
          <th style="text-align: right">After SFT 500 /100</th>
          <th style="text-align: right">After SFT 1,000 /100</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: right">2 billion</td>
          <td style="text-align: right">0</td>
          <td style="text-align: right">24</td>
          <td style="text-align: right">39</td>
      </tr>
      <tr>
          <td style="text-align: right">5 billion</td>
          <td style="text-align: right">0</td>
          <td style="text-align: right">49</td>
          <td style="text-align: right">52</td>
      </tr>
      <tr>
          <td style="text-align: right">10 billion</td>
          <td style="text-align: right">8</td>
          <td style="text-align: right">56</td>
          <td style="text-align: right">65</td>
      </tr>
  </tbody>
</table>
<p>The longer SFT on top of the 10 billion reached <strong>68 out of 100</strong> on the development set and <strong>58 out of 100</strong> on the held-out one. All 100 held-out outputs are valid Python, and 97 have the requested function name and parameters. Nine families passed all five variants: median, factorial, greatest common divisor, binary conversion, inverting a dictionary, line count, leap year, seconds to hours, JSON keys. Running total ended at four out of five.</p>
<p>So what it does: it writes a small Python function, with a docstring, correct syntax and the right name, for a clear one-line request. Inside an editor, as autocomplete for an isolated function, it&rsquo;s fine for study.</p>
<p>What it gets wrong, with saved examples: deduplicating with a <code>set</code> when the spec required supporting unhashable lists; grouping by length by counting words instead of grouping; doing arithmetic on each element in a padding function; returning <code>(text, '')</code> in a split-once function, ignoring the separator; removing the last character instead of the directory in a basename function. Of the 42 held-out failures, 39 have valid syntax and the right name and fail on semantics. The hard part was never looking like code.</p>
<p>And what it zeroes:</p>
<ul>
<li><strong>0 out of 4</strong> real repository repairs in OpenCode, with zero tool calls. Asked to read, edit and test a file, it returned text with code. In one of the tasks, it renamed the function to <code>fix_mean</code>.</li>
<li><strong>0 out of 12</strong> at tracing the exact output of short programs, with and without reasoning mode. Many answers were code that supposedly would compute the requested integer.</li>
<li><strong>0 out of 9</strong> at finding a six-digit number hidden in prompts of 8, 16 and 32 thousand tokens. The 32K-token allocation works; the useful memory, none.</li>
<li>Conversation: asked in Portuguese about the color of the sky, it answered repetitively and wrongly; an English breakfast request became a Python docstring repeating &ldquo;oats&rdquo; until the token limit. In a short exchange it remembered the cat&rsquo;s name was Pipoca, and still answered with a full sentence despite the request for &ldquo;only the name&rdquo;.</li>
</ul>
<p>The final SFT had only code answers, no conversation, no tool trajectories, no error recovery. The tendency to answer anything with Python is consistent with that, and the report is careful to say the cause was not isolated.</p>
<h2>What it would still take for a simple agent in OpenCode<span class="hx:absolute hx:-mt-20" id="what-it-would-still-take-for-a-simple-agent-in-opencode"></span>
    <a href="#what-it-would-still-take-for-a-simple-agent-in-opencode" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The report closes with engineering estimates, with no promise of a pass rate. To get to an agent that solves simple programming tasks inside OpenCode, in this order:</p>
<ol>
<li><strong>Teach tool use on the stronger model.</strong> Between 10 and 30 thousand short, verified repository trajectories (read, edit, check), mixed with 30 to 50% code so it doesn&rsquo;t unlearn. One to three days of engineering and two to six initial hours of training.</li>
<li><strong>Correct its own mistakes.</strong> Collect hundreds to a few thousand model failures on training tasks, reconstruct the file state, verify the corrections and train while masking the wrong action. Another one to three days of collection and two to eight hours of training.</li>
<li><strong>Bring the conversation back.</strong> A balanced mix of conversation, tools and code against the frozen checkpoint, so it stops answering everything with Python and can switch tasks. One to four hours of training.</li>
<li><strong>Real long context.</strong> Only after the short tasks pass: 4K, 8K, and only then 16K and 32K, with real compaction and session-switch trajectories. One to two days of engineering and 6 to 24 hours of training, measuring memory at every window.</li>
<li><strong>More foundation, with better data.</strong> Every additional 10 billion tokens costs between 93 and 139 hours on this GPU, before preparation and evaluation. And repeating the same 3.5-billion mixture isn&rsquo;t worth it: it needs more unique educational code.</li>
</ol>
<p>None of those steps was executed. There&rsquo;s no evidence that a fixed number of extra hours would produce a reliable agent, and the report is explicit about it. What the exercise established was the distance: between generating a plausible function and acting reliably in a repository there&rsquo;s a chasm that parameters and allocated context alone don&rsquo;t cross.</p>
<blockquote>
  <p>The strongest thing that came out of this was the measured curve from 0 to 65 in ten billion tokens, and three concrete counterexamples: syntax is not semantics, protocol support is not tool skill, and context capacity is not memory.</p>

</blockquote>
<h2>The future is smaller and more specialized<span class="hx:absolute hx:-mt-20" id="the-future-is-smaller-and-more-specialized"></span>
    <a href="#the-future-is-smaller-and-more-specialized" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>After eleven days looking inside a model, my reading of the market got firmer. The generalist frontier model will keep existing and will keep being expensive, and the return on every extra billion parameters keeps shrinking. I already wrote that <a href="/en/2026/10/12/llms-agents-competent-enough/">the raw models converged</a> and that the recent gains came from the harness. The exercise gave me the other half of the argument.</p>
<p>Mixture of experts is the industry admitting it on the inside. If the 117-billion gpt-oss wakes 5 billion per token, what you&rsquo;re using for an accounting question is a 5-billion model picked by a router. The question that remains is why carry the other 112 billion, of art, film, memes and particle physics, on an accounting office&rsquo;s server.</p>
<p>Imagine a local model, built for accountants. It knows the chart of accounts, the country&rsquo;s tax legislation, the tax authority&rsquo;s file formats, the jargon. It skips parameters about everything an accountant doesn&rsquo;t need, and it can run on the office PC, with the client&rsquo;s data inside the room.</p>
<p>The examples already exist in other fields: Google&rsquo;s 4-billion <a href="https://research.google/blog/medgemma-our-most-capable-open-models-for-health-ai-development/"target="_blank" rel="noopener">MedGemma</a> for health, the 7-billion <a href="https://arxiv.org/abs/2403.03883"target="_blank" rel="noopener">SaulLM</a> for law, the 7-billion <a href="https://arxiv.org/abs/2503.16252"target="_blank" rel="noopener">Fin-R1</a> for financial reasoning. In 2023 <a href="https://arxiv.org/abs/2303.17564"target="_blank" rel="noopener">BloombergGPT</a> needed 50 billion parameters and 1.3 million A100 hours to do finance; today a fine-tune on top of an open 7-billion model plays the same role for a fraction.</p>
<p>What changed to make this viable was the agent. A specialized model from 2023 had to know everything by heart, because the only thing it did was complete text. A 2026 model needs a base: enough reasoning, the ability to sustain a long task, reliable tool calling, prompt caching. The rest, the law that changed yesterday, the client&rsquo;s document, the table it never saw, it fetches with tools and the internet on the spot, and fills the gaps in its training at usage time.</p>
<p><a href="https://arxiv.org/abs/2506.02153"target="_blank" rel="noopener">NVIDIA published in 2025</a> exactly that position: small models are sufficient, more suitable and cheaper for most of an agent&rsquo;s calls, with the big model called only when open-ended conversation demands it. Apple already <a href="https://machinelearning.apple.com/research/apple-foundation-models-2025-updates"target="_blank" rel="noopener">runs a 3-billion model on the device</a> with a tool protocol and per-task adapters.</p>
<p>It&rsquo;s the same reasoning as the <a href="/en/2026/10/10/which-jev-like-ai-decision-classifier-model-is-best/">decision models I tested</a> last week: a 4-billion classifier trained for a task beats a generic chat model of the same size at it, in a fraction of the response time. Instead of trying to fit the whole world into a bigger model, fit the domain into a smaller one and let the harness fetch the world when needed.</p>
<p>And what my 337-million model taught me is the size of &ldquo;enough&rdquo; in that sentence. It knows the syntax and knows the names of things. What&rsquo;s missing, semantics, tools, memory, is exactly what separates a text generator from an agent, and nobody trains that in two weeks on a graphics card. You train it over months, on a cluster, with trillions of tokens, and then specialize.</p>
<p>The code is in the <a href="https://github.com/akitaonrails/microgpt"target="_blank" rel="noopener">repository</a>, with the full closeout report, for anyone who wants to pick up where I left off. It&rsquo;s just for study. And it was the best study of the year.</p>
]]></content:encoded><category>artificial-intelligence</category><category>llms</category><category>machine-learning</category><category>local-models</category></item><item><title>Which Jev-like AI Decision/Classifier Model Is Best?</title><link>https://akitaonrails.github.io/en/2026/10/10/which-jev-like-ai-decision-classifier-model-is-best/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/10/10/which-jev-like-ai-decision-classifier-model-is-best/</guid><pubDate>Sat, 10 Oct 2026 12:00:00 -0300</pubDate><description>&lt;p&gt;Last week I published &lt;a href="https://akitaonrails.github.io/en/2026/10/03/understanding-the-jev-hype-what-is-it-for/"&gt;a long post about Jev&lt;/a&gt;, TypeSafe&amp;rsquo;s hosted classifier that became all the rage in September. The short version of that post: Jev is a classifier, something that has existed for decades, and the &amp;ldquo;193x faster&amp;rdquo; amazement only exists because too many people were using chat LLMs as expensive, slow classifiers. I reproduced Paulo Câmara&amp;rsquo;s essay &lt;em&gt;The student who marks answer A&lt;/em&gt;, introduced the open alternatives Clef and Laya, and to compare the models side by side I leaned on the &lt;a href="https://huggingface.co/spaces/multimodalart/jev-decision-index"target="_blank" rel="noopener"&gt;Decision Index&lt;/a&gt;, a public ranking that already existed.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Last week I published <a href="/en/2026/10/03/understanding-the-jev-hype-what-is-it-for/">a long post about Jev</a>, TypeSafe&rsquo;s hosted classifier that became all the rage in September. The short version of that post: Jev is a classifier, something that has existed for decades, and the &ldquo;193x faster&rdquo; amazement only exists because too many people were using chat LLMs as expensive, slow classifiers. I reproduced Paulo Câmara&rsquo;s essay <em>The student who marks answer A</em>, introduced the open alternatives Clef and Laya, and to compare the models side by side I leaned on the <a href="https://huggingface.co/spaces/multimodalart/jev-decision-index"target="_blank" rel="noopener">Decision Index</a>, a public ranking that already existed.</p>
<p>Someone else&rsquo;s ranking is useful, but I wanted to get my hands dirty. So I built my own benchmark, with my own cases, half in Portuguese, and ran the open models on my <a href="/en/2026/03/31/minisforum-ms-s1-max-amd-ai-max-395-review/">home server with Strix Halo</a>, the same Ryzen AI Max+ 395 I use to test local LLMs. The hosted ones (Jev, the two Clefs from Cloudflare, OpenAI Decisions and Microsoft Decision-1 via OpenRouter) ran through their APIs. Everything, including the cases, the raw responses and the scoring scripts, is public in the <a href="https://github.com/akitaonrails/jev-docs-benchmarks"target="_blank" rel="noopener">benchmark repository</a>.</p>
<blockquote>
  <p><strong>TL;DR:</strong> the top now has two, both open-weights. <strong>Perplexity Decider 1.1 27B</strong> and the just-released <strong>Decision 3.0</strong> (vLLM Semantic Router) tied at <strong>99.4%</strong> on the compact cases, with 100% in Portuguese, and identical labels on all 1,024 classifications in the benchmark. The price: about 49 GB of weights and over a second per decision. In the cloud, <strong>Microsoft Decision-1</strong> tied <strong>Jev</strong> at 97.6% while costing a third: it&rsquo;s the cheapest hosted one I measured, and the first candidate to evaluate. Jev answers faster and errs differently. <strong>OpenAI Decisions</strong> debuted tied with the best local 4B, Jebadiah, and has the lowest hosted latency. <strong>Julia-1</strong>, the smallest and fastest of all (12 ms), scored at coin-flip level: fresh out of the factory, it&rsquo;s a no-go. Jump to the <a href="#the-full-table">full table</a>, the <a href="#how-much-it-costs-in-support-emails">cost in support emails</a>, <a href="#which-model-for-which-situation">which model for which situation</a>, <a href="#how-hard-it-is-to-run-the-best-open-models-on-your-machine">how hard it is to run the best open ones locally</a> or the <a href="#highlights-and-gotchas">gotchas</a>.</p>

</blockquote>
<h2>How I tested<span class="hx:absolute hx:-mt-20" id="how-i-tested"></span>
    <a href="#how-i-tested" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The central idea was to measure more than &ldquo;who gets the most right.&rdquo; A decision model has a design goal, and comparing a 322-million-parameter encoder made to run on weak devices with a hosted service of undisclosed size on accuracy percentage alone is unfair to both. So I measured accuracy, latency, cost and behavior under stress, and at the end I split the recommendations by situation.</p>
<h3>The two case sets<span class="hx:absolute hx:-mt-20" id="the-two-case-sets"></span>
    <a href="#the-two-case-sets" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>I built two test sets, frozen in git before any inference, with the dataset hash recorded alongside the responses. No model receives the expected answer, the justification or the case identifier. Only the text, the instructions and the options.</p>
<p>The <strong>v1 set</strong> has 13 operational use cases, in English, each with an explicit fictional policy:</p>
<ul>
<li>support ticket severity</li>
<li>support routing</li>
<li>refund policy</li>
<li>incident escalation</li>
<li>tool approval (whether an agent may or may not execute a given action)</li>
<li>email fraud</li>
<li>document type</li>
<li>evidence support</li>
<li>search relevance</li>
<li>moderation</li>
<li>order routing</li>
<li>lead qualification</li>
<li>numeric-threshold triage</li>
</ul>
<p>There are 10 scenarios per use case, each presented four ways: original, options in reversed order, options with opaque names (c0, c1, c2) and original plus a &ldquo;reviewer&rdquo; comment suggesting the wrong answer, which the instructions say to ignore. That&rsquo;s 520 classifications per model. Add 24 probability probes in situations with no information at all (fair coin, six-sided die), where the right answer is mathematical.</p>
<p>The <strong>v2 set</strong> was made to cover what v1 leaves out: common semantics and Portuguese. There are 7 tasks, with 12 scenarios each, and every scenario written in English and in Brazilian Portuguese with the same answer and the same option order:</p>
<ul>
<li>sentiment (author versus quotation, negation, sarcasm)</li>
<li>current intent (withdrawn request, competing actions, hypothesis)</li>
<li>logical implication (quantifiers, absent fact, temporal order)</li>
<li>search relevance (all, part or none of what was asked)</li>
<li>sensitive data handling (fictional secret versus mention, redaction, revocation)</li>
<li>current incident status (timeline order, verified versus unverified)</li>
<li>threshold policy (inclusive bounds, units, AND/OR with a missing value)</li>
</ul>
<p>Each pair runs under three conditions:</p>
<ul>
<li><strong>compact</strong>: just the record and the rule</li>
<li><strong>with examples</strong>: one solved example per class, fixed and the same for everyone</li>
<li><strong>with distractors</strong>: ten irrelevant archive notes before the record that matters, to stress the context</li>
</ul>
<p>That&rsquo;s 504 classifications per model.</p>
<blockquote>
  <p>The cases were written with AI help and reviewed against the policies, without independent human annotation. It&rsquo;s a small, synthetic benchmark: 130 scenario families in v1 and 84 in v2. The variants are correlated, so 520 responses are 130 scenarios seen four ways. Treat the numbers as a snapshot of these cases, and test with your own before deciding.</p>

</blockquote>
<h3>The 23 competitors<span class="hx:absolute hx:-mt-20" id="the-23-competitors"></span>
    <a href="#the-23-competitors" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Five ran in the cloud, through the official API: <strong>Jev</strong> (version 1.13.0, from TypeSafe), <strong>Clef</strong> (27B, Cloudflare Workers AI), <strong>Clef-flash</strong> (9B, same), <strong>OpenAI Decisions</strong> (the <code>gpt-6-luna</code> alias, in public beta) and <strong>Microsoft Decision-1</strong> (9B, post-trained from Qwen3.5-9B, served via OpenRouter on Azure). The other eighteen ran on the Strix Halo, via ROCm, with weights pinned by Hugging Face commit hash, inside a Docker container. The first three (Laya, Laya typed and Clef-flash) shared one container; the rest ran one at a time, with Julia-1 measured twice, on GPU and on CPU. The server has 64 GiB of GPU-addressable memory.</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th style="text-align: right">Nominal size</th>
          <th>Origin</th>
          <th>Where it ran</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Jev 1.13.0</td>
          <td style="text-align: right">undisclosed</td>
          <td>TypeSafe, proprietary</td>
          <td>API</td>
      </tr>
      <tr>
          <td>OpenAI Decisions (<code>gpt-6-luna</code>)</td>
          <td style="text-align: right">undisclosed</td>
          <td>OpenAI, proprietary, beta</td>
          <td>API</td>
      </tr>
      <tr>
          <td>Microsoft Decision-1</td>
          <td style="text-align: right">9B</td>
          <td>Microsoft, proprietary</td>
          <td>API (OpenRouter)</td>
      </tr>
      <tr>
          <td>Clef</td>
          <td style="text-align: right">27B</td>
          <td>Cloudflare, open weights</td>
          <td>API</td>
      </tr>
      <tr>
          <td>Clef-flash</td>
          <td style="text-align: right">9B</td>
          <td>Cloudflare, open weights</td>
          <td>API and local</td>
      </tr>
      <tr>
          <td>Perplexity Decider 1.1 27B</td>
          <td style="text-align: right">27B</td>
          <td>Perplexity, Apache 2.0</td>
          <td>local</td>
      </tr>
      <tr>
          <td>Decision 3.0 (d3)</td>
          <td style="text-align: right">27B</td>
          <td>vLLM Semantic Router, Apache 2.0</td>
          <td>local</td>
      </tr>
      <tr>
          <td>Decision 3.0 Edge</td>
          <td style="text-align: right">0.6B</td>
          <td>vLLM Semantic Router, Apache 2.0</td>
          <td>local</td>
      </tr>
      <tr>
          <td>Jebadiah 4B v2</td>
          <td style="text-align: right">4B</td>
          <td>Frontier Infra, Apache 2.0</td>
          <td>local</td>
      </tr>
      <tr>
          <td>Kev 0.8B / 4B / 9B</td>
          <td style="text-align: right">0.8B / 4B / 9B</td>
          <td>Jared Palmer, Apache 2.0</td>
          <td>local</td>
      </tr>
      <tr>
          <td>Mapika Decider 4B</td>
          <td style="text-align: right">4.2B</td>
          <td>Mapika, Apache 2.0</td>
          <td>local</td>
      </tr>
      <tr>
          <td>Eikos 4B</td>
          <td style="text-align: right">4B</td>
          <td>Caio Vicentino, MIT, trained on EN and PT</td>
          <td>local</td>
      </tr>
      <tr>
          <td>Simple-Jev + Qwen3.5 4B</td>
          <td style="text-align: right">4B</td>
          <td>Featherless, token-reading server over a stock model</td>
          <td>local</td>
      </tr>
      <tr>
          <td>Laya / Laya typed-decisions</td>
          <td style="text-align: right">421M</td>
          <td>Convai Innovations, ModernBERT encoder</td>
          <td>local</td>
      </tr>
      <tr>
          <td>Laya multilingual</td>
          <td style="text-align: right">322M</td>
          <td>Convai Innovations, mmBERT encoder</td>
          <td>local</td>
      </tr>
      <tr>
          <td>Julia-1</td>
          <td style="text-align: right">144M</td>
          <td>Supersonic Labs, Apache 2.0, mmBERT encoder</td>
          <td>local, GPU and CPU</td>
      </tr>
      <tr>
          <td>GLiNER2.5-Decide / multi-Decide</td>
          <td style="text-align: right">340M / 287M</td>
          <td>Fastino, Apache 2.0</td>
          <td>local</td>
      </tr>
  </tbody>
</table>
<p>The order up there is just organizational, hosted first and local ones by family. The real ranking is in the <a href="#the-full-table">results table</a>.</p>
<p>Everyone received exactly the same text, the same instructions and the same options, with no per-model prompt tuning and no adjustments after seeing results (Simple-Jev uses its own server&rsquo;s fixed prompt policy). The exception is <strong>GLiNER</strong>, whose native API takes labels instead of probability distributions. It runs on a separate track, v2 only, with explicit mapping and no made-up calibration metrics.</p>
<p><strong>Simple-Jev</strong> deserves an explanation. It&rsquo;s a server that takes a stock LLM (here Qwen3.5 4B, with no decision training at all) and reads the probability of the answer tokens instead of generating text. It&rsquo;s the baseline: if a &ldquo;decision&rdquo; model loses to this, the specialized training bought nothing.</p>
<p>Two protocol notes. <strong>OpenAI Decisions</strong> answers through a native typed-decisions endpoint, and the alias the API returned is not an immutable snapshot: OpenAI can swap the weights behind the name, like Cloudflare does with Clef-flash. And <strong>Perplexity Decider</strong> is not a stock chat LLM: it has a separate decision head, with non-causal attention and a calibration temperature saved in the weights. A generic chat server does not reproduce its behavior. <strong>Microsoft Decision-1</strong> speaks native SystemOne through OpenRouter&rsquo;s decisions API, and the served identifier is dated (<code>microsoft-decision-1-20261009</code>), so it&rsquo;s not an immutable weight either. It doesn&rsquo;t show up in OpenRouter&rsquo;s text-model catalog: to find it, you have to filter by the <code>decisions</code> modality. <strong>Decision 3.0</strong> is the new family from the vLLM Semantic Router project, Apache 2.0, derived from Qwen, with six checkpoints (d3, flash, mini, nano, lite, edge) and native SystemOne. I tested the flagship (d3) and the smallest (d3-edge); the four in between are for another round.</p>
<h3>What was measured<span class="hx:absolute hx:-mt-20" id="what-was-measured"></span>
    <a href="#what-was-measured" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li>Accuracy per task, per language and per condition, always on top of the per-option probabilities the model returns, ignoring the vendor&rsquo;s &ldquo;confidence&rdquo; field.</li>
<li>Brier and ECE for calibration.</li>
<li>Median and p95 latency as seen by the client, including the network: WAN for the hosted ones, LAN for the local ones. These are deployment observations, and comparing hardware would require another test.</li>
<li>Input tokens reported by the API, priced at the October 10 price table, before any allowance.</li>
<li>Bootstrap confidence intervals over scenario families, instead of individual responses.</li>
</ul>
<p>I left local cost unmeasured. Energy, hardware amortization and maintenance were left out, so local models show up as &ldquo;not measured,&rdquo; because &ldquo;zero&rdquo; would be a lie.</p>
<h2>The full table<span class="hx:absolute hx:-mt-20" id="the-full-table"></span>
    <a href="#the-full-table" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The main column is accuracy on the 168 compact v2 cases (84 in English, 84 in Portuguese). The v1 column uses the 130 original scenarios. Latency is the median seen by the client across the 504 v2 calls. Cost is the list price per 10 thousand compact decisions, computed from the tokens the API reported.</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th style="text-align: right">Compact EN / PT-BR</th>
          <th style="text-align: right">Compact combined</th>
          <th style="text-align: right">v1 originals</th>
          <th style="text-align: right">Median latency</th>
          <th style="text-align: right">10 thousand decisions</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Perplexity Decider 27B</td>
          <td style="text-align: right">98.8% / 100.0%</td>
          <td style="text-align: right"><strong>99.4%</strong></td>
          <td style="text-align: right">99.2%</td>
          <td style="text-align: right">1,547 ms (LAN)</td>
          <td style="text-align: right">not measured</td>
      </tr>
      <tr>
          <td>Decision 3.0 (27B)</td>
          <td style="text-align: right">98.8% / 100.0%</td>
          <td style="text-align: right"><strong>99.4%</strong></td>
          <td style="text-align: right">99.2%</td>
          <td style="text-align: right">1,420 ms (LAN)</td>
          <td style="text-align: right">not measured</td>
      </tr>
      <tr>
          <td>Jev</td>
          <td style="text-align: right">97.6% / 97.6%</td>
          <td style="text-align: right">97.6%</td>
          <td style="text-align: right">96.2%</td>
          <td style="text-align: right">255 ms (WAN)</td>
          <td style="text-align: right">US$ 0.19</td>
      </tr>
      <tr>
          <td>Microsoft Decision-1</td>
          <td style="text-align: right">95.2% / 100.0%</td>
          <td style="text-align: right">97.6%</td>
          <td style="text-align: right">95.4%</td>
          <td style="text-align: right">422 ms (WAN)</td>
          <td style="text-align: right">US$ 0.065</td>
      </tr>
      <tr>
          <td>Clef (27B)</td>
          <td style="text-align: right">97.6% / 94.0%</td>
          <td style="text-align: right">95.8%</td>
          <td style="text-align: right">94.6%</td>
          <td style="text-align: right">302 ms (WAN)</td>
          <td style="text-align: right">US$ 0.63</td>
      </tr>
      <tr>
          <td>Jebadiah 4B v2</td>
          <td style="text-align: right">91.7% / 89.3%</td>
          <td style="text-align: right">90.5%</td>
          <td style="text-align: right">85.4%</td>
          <td style="text-align: right">205 ms (LAN)</td>
          <td style="text-align: right">not measured</td>
      </tr>
      <tr>
          <td>OpenAI Decisions</td>
          <td style="text-align: right">90.5% / 90.5%</td>
          <td style="text-align: right">90.5%</td>
          <td style="text-align: right">93.1%</td>
          <td style="text-align: right">222 ms (WAN)</td>
          <td style="text-align: right">US$ 0.23</td>
      </tr>
      <tr>
          <td>Kev 9B</td>
          <td style="text-align: right">92.9% / 83.3%</td>
          <td style="text-align: right">88.1%</td>
          <td style="text-align: right">87.7%</td>
          <td style="text-align: right">561 ms (LAN)</td>
          <td style="text-align: right">not measured</td>
      </tr>
      <tr>
          <td>Clef-flash (9B) hosted</td>
          <td style="text-align: right">91.7% / 83.3%</td>
          <td style="text-align: right">87.5%</td>
          <td style="text-align: right">90.8%</td>
          <td style="text-align: right">394 ms (WAN)</td>
          <td style="text-align: right">US$ 0.10</td>
      </tr>
      <tr>
          <td>Clef-flash (9B) local</td>
          <td style="text-align: right">89.3% / 85.7%</td>
          <td style="text-align: right">87.5%</td>
          <td style="text-align: right">90.8%</td>
          <td style="text-align: right">618 ms (LAN)</td>
          <td style="text-align: right">not measured</td>
      </tr>
      <tr>
          <td>Mapika Decider 4B</td>
          <td style="text-align: right">89.3% / 85.7%</td>
          <td style="text-align: right">87.5%</td>
          <td style="text-align: right">86.9%</td>
          <td style="text-align: right">186 ms (LAN)</td>
          <td style="text-align: right">not measured</td>
      </tr>
      <tr>
          <td>Kev 4B</td>
          <td style="text-align: right">88.1% / 86.9%</td>
          <td style="text-align: right">87.5%</td>
          <td style="text-align: right">82.3%</td>
          <td style="text-align: right">246 ms (LAN)</td>
          <td style="text-align: right">not measured</td>
      </tr>
      <tr>
          <td>Eikos 4B</td>
          <td style="text-align: right">85.7% / 79.8%</td>
          <td style="text-align: right">82.7%</td>
          <td style="text-align: right">87.7%</td>
          <td style="text-align: right">235 ms (LAN)</td>
          <td style="text-align: right">not measured</td>
      </tr>
      <tr>
          <td>Simple-Jev + Qwen3.5 4B</td>
          <td style="text-align: right">77.4% / 71.4%</td>
          <td style="text-align: right">74.4%</td>
          <td style="text-align: right">85.4%</td>
          <td style="text-align: right">1,349 ms (LAN)</td>
          <td style="text-align: right">not measured</td>
      </tr>
      <tr>
          <td>Kev 0.8B</td>
          <td style="text-align: right">64.3% / 61.9%</td>
          <td style="text-align: right">63.1%</td>
          <td style="text-align: right">63.1%</td>
          <td style="text-align: right">86 ms (LAN)</td>
          <td style="text-align: right">not measured</td>
      </tr>
      <tr>
          <td>Decision 3.0 Edge (0.6B)</td>
          <td style="text-align: right">67.9% / 53.6%</td>
          <td style="text-align: right">60.7%</td>
          <td style="text-align: right">65.4%</td>
          <td style="text-align: right">45 ms (LAN)</td>
          <td style="text-align: right">not measured</td>
      </tr>
      <tr>
          <td>GLiNER2.5-Decide (EN)</td>
          <td style="text-align: right">54.8% / 50.0%</td>
          <td style="text-align: right">52.4%</td>
          <td style="text-align: right">didn&rsquo;t run</td>
          <td style="text-align: right">208 ms (LAN)</td>
          <td style="text-align: right">not measured</td>
      </tr>
      <tr>
          <td>Laya typed-decisions</td>
          <td style="text-align: right">50.0% / 35.7%</td>
          <td style="text-align: right">42.9%</td>
          <td style="text-align: right">43.8%</td>
          <td style="text-align: right">43 ms (LAN)</td>
          <td style="text-align: right">not measured</td>
      </tr>
      <tr>
          <td>Laya (EN)</td>
          <td style="text-align: right">44.0% / 41.7%</td>
          <td style="text-align: right">42.9%</td>
          <td style="text-align: right">43.8%</td>
          <td style="text-align: right">41 ms (LAN)</td>
          <td style="text-align: right">not measured</td>
      </tr>
      <tr>
          <td>Laya multilingual</td>
          <td style="text-align: right">35.7% / 38.1%</td>
          <td style="text-align: right">36.9%</td>
          <td style="text-align: right">43.8%</td>
          <td style="text-align: right">19 ms (LAN)</td>
          <td style="text-align: right">not measured</td>
      </tr>
      <tr>
          <td>GLiNER2.5-multi-Decide</td>
          <td style="text-align: right">32.1% / 35.7%</td>
          <td style="text-align: right">33.9%</td>
          <td style="text-align: right">didn&rsquo;t run</td>
          <td style="text-align: right">63 ms (LAN)</td>
          <td style="text-align: right">not measured</td>
      </tr>
      <tr>
          <td>Julia-1 (GPU)</td>
          <td style="text-align: right">38.1% / 25.0%</td>
          <td style="text-align: right">31.5%</td>
          <td style="text-align: right">38.5%</td>
          <td style="text-align: right"><strong>12 ms</strong> (LAN)</td>
          <td style="text-align: right">not measured</td>
      </tr>
      <tr>
          <td>Julia-1 (CPU)</td>
          <td style="text-align: right">38.1% / 25.0%</td>
          <td style="text-align: right">31.5%</td>
          <td style="text-align: right">36.9%</td>
          <td style="text-align: right">71 ms (LAN)</td>
          <td style="text-align: right">not measured</td>
      </tr>
  </tbody>
</table>
<p>To calibrate your reading: the tasks have three or four options, so a uniform guess gives about 31% on both sets. &ldquo;Near 50%&rdquo; here is already well above the coin, and &ldquo;near 33%&rdquo; is the coin. Julia-1 landed at 31.5%: the pretty name hides a guy flipping a coin.</p>
<p>The intervals matter. Perplexity&rsquo;s edge over Jev on v2 is under 2 points and the bootstrap interval touches zero: it&rsquo;s the measured lead on this set, not a proven superiority forever. And Decision 3.0 tied Perplexity at the limit of what this set can distinguish: <strong>identical labels on the 1,024 classifications</strong>, with both missing the same two cases (the English relevance one on v2 and the disputed severity case on v1, already in the errata). Microsoft tied Jev on the aggregate, with the interval centered on zero, but an aggregate tie is not interchangeability: they miss different cases, and Microsoft inverts the language pattern, missing more in English than in Portuguese. Jev and Clef also remain under 2 points apart, on the same tier. The 90% squad (Jebadiah, OpenAI) sits 7 points behind Jev, and from there down every difference is clear.</p>
<p>Each task cell has only 12 scenarios per language, so one more correct answer moves the cell by more than 8 points. A tie on a specific task is a lead for a pilot. The per-task tables are in the <a href="https://github.com/akitaonrails/jev-docs-benchmarks/blob/master/benchmark/reports/decision3-comparison-v2/results.md"target="_blank" rel="noopener">repository</a>.</p>
<h2>How much it costs, in support emails<span class="hx:absolute hx:-mt-20" id="how-much-it-costs-in-support-emails"></span>
    <a href="#how-much-it-costs-in-support-emails" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Price per million tokens is a number nobody can feel. List prices on October 11 are US$ 0.042 per million input tokens on Jev, US$ 0.24 on Clef, US$ 0.038 on Clef-flash and US$ 0.10 on OpenAI Decisions. Output is free on all five. Microsoft Decision-1 charges the same US$ 0.042 as Jev per million, via OpenRouter, but reported a third of the input tokens for the same texts (about 26 thousand versus Jev&rsquo;s 77 thousand on compact v2), and nobody can explain the tokenization difference. In measured practice, that makes it the cheapest hosted one of the bunch.</p>
<p>Clef-flash cost US$ 0.09 on the 9th: Cloudflare cut the price by more than half in the middle of my test. Jev reports considerably more tokens than Cloudflare for the same text, about 45% more on v2, and that&rsquo;s already in the math below.</p>
<p>Imagine an operation with <strong>10 thousand support emails per month</strong> to classify into three or four buckets, with short texts similar to my cases:</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th style="text-align: right">10k emails / month</th>
          <th style="text-align: right">1 million emails</th>
          <th style="text-align: right">Expected errors in 10k (by compact accuracy)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Microsoft Decision-1</td>
          <td style="text-align: right">US$ 0.065</td>
          <td style="text-align: right">US$ 6.45 (about R$ 32)</td>
          <td style="text-align: right">~240</td>
      </tr>
      <tr>
          <td>Clef-flash</td>
          <td style="text-align: right">US$ 0.10</td>
          <td style="text-align: right">US$ 10 (about R$ 50)</td>
          <td style="text-align: right">~1,250</td>
      </tr>
      <tr>
          <td>Jev</td>
          <td style="text-align: right">US$ 0.19</td>
          <td style="text-align: right">US$ 19 (about R$ 95)</td>
          <td style="text-align: right">~240</td>
      </tr>
      <tr>
          <td>OpenAI Decisions</td>
          <td style="text-align: right">US$ 0.23</td>
          <td style="text-align: right">US$ 23 (about R$ 115)</td>
          <td style="text-align: right">~950</td>
      </tr>
      <tr>
          <td>Clef</td>
          <td style="text-align: right">US$ 0.63</td>
          <td style="text-align: right">US$ 63 (about R$ 315)</td>
          <td style="text-align: right">~420</td>
      </tr>
  </tbody>
</table>
<blockquote>
  <p>In reais, at Friday&rsquo;s exchange rate (R$ 4.99), 10 thousand decisions on Jev cost one real. A million costs less than a hundred reais. For any company with a million emails to classify, that vanishes into the budget.</p>

</blockquote>
<p>One caveat about proportions: my cases are short, about 450 tokens per compact case by Jev&rsquo;s count. A real customer email, with quoted thread history, can be three to five times larger, and cost scales linearly with that. Even so, we&rsquo;re talking cents per thousand.</p>
<p>What this math shows is that the price difference between the hosted models is irrelevant next to the cost of being wrong. Jev costs 9 US cents more than Clef-flash per 10 thousand decisions, and in those same 10 thousand it makes about a thousand fewer errors. If a misrouted email costs more than a hundredth of a US cent in human rework, Jev pays for itself. This math assumes every error costs the same, which is never true: sending a critical ticket to the routine queue costs far more than the reverse.</p>
<p>Cloudflare&rsquo;s allowance changes the math for small players. It&rsquo;s 10 thousand &ldquo;neurons&rdquo; per day, shared by the whole account, resetting at midnight UTC. With my input sizes, that&rsquo;s about 1,500 full-Clef classifications per day or almost 10 thousand Clef-flash ones, free. If you have 1,000 emails a day and nothing else running on the account, Clef comes out at zero cost with top-tier quality.</p>
<p>And local? Energy is what the benchmark didn&rsquo;t measure. From what I see on my Strix Halo, the whole machine stays under 100 W under load. With Jebadiah at 200 ms per decision, 10 thousand decisions are about 35 machine minutes, under 0.1 kWh, a few cents of a real on the power bill.</p>
<p>The real local cost is the machine, your hours configuring ROCm and containers, and keeping it all standing. Whoever already has the hardware for another reason has low marginal cost. Whoever would buy the machine just for this is buying a problem of thousands of reais to save about a hundred reais per million.</p>
<h2>Which model for which situation<span class="hx:absolute hx:-mt-20" id="which-model-for-which-situation"></span>
    <a href="#which-model-for-which-situation" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>You want accuracy and want to operate nothing: Jev<span class="hx:absolute hx:-mt-20" id="you-want-accuracy-and-want-to-operate-nothing-jev"></span>
    <a href="#you-want-accuracy-and-want-to-operate-nothing-jev" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>In the cloud, Jev is still the name. It won or tied practically every cell among the hosted ones, and only Microsoft tied it on the aggregate. In English it was 100% on five of the seven v2 tasks, and in Portuguese it repeated 97.6% on the aggregate without losing anything in translation, something no other model above the coin managed. On v1 it kept its 96.2% on the original with the malicious &ldquo;reviewer&rdquo; comment injected; Clef even gained one case, and Kev 4B also stayed put, just at a much lower tier. The 255 ms WAN latency came in 30 ms behind OpenAI&rsquo;s, and it costs 19 US cents per 10 thousand decisions.</p>
<p>The trade-off is the one I already discussed in the previous post: it&rsquo;s proprietary, hosted, of undisclosed size, and you depend on TypeSafe keeping the behavior underneath you. And it makes mistakes. It treated an invoice ID called &ldquo;PASSWORD-523&rdquo; as a usable credential, even with the record explicitly saying it doesn&rsquo;t authenticate anyone.</p>
<h3>You want Jev&rsquo;s level paying a third: Microsoft Decision-1<span class="hx:absolute hx:-mt-20" id="you-want-jevs-level-paying-a-third-microsoft-decision-1"></span>
    <a href="#you-want-jevs-level-paying-a-third-microsoft-decision-1" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Microsoft entered this market quietly, and entered well. Decision-1 is a 9B post-trained from Qwen3.5-9B, served via OpenRouter, and it was the first model to tie Jev on the compact aggregate: 97.6%, with the bootstrap interval centered on zero. The detail that won me over: <strong>100% in Portuguese</strong>, better than its own English (95%), inverting the pattern of the entire rest of the bunch. On v1, the reversed-order and malicious-comment variants didn&rsquo;t take down a single answer, and it got the critical ticket that Clef and OpenAI missed.</p>
<p>And it&rsquo;s the cheapest hosted one I measured: US$ 0.065 per 10 thousand decisions, a third of Jev, charging the same per-token rate but reporting a third of the tokens. The savings come from the token count, and I can&rsquo;t explain the exact mechanism, so treat it as a measurement, not a promise.</p>
<p>The weak spots exist. Latency is the worst among the hosted ones (422 ms median, versus Jev&rsquo;s 255). On the compact cases it&rsquo;s clean of confident errors, but add context and Portuguese errors show up with 99.5% confidence: the &ldquo;perfect Portuguese&rdquo; pattern doesn&rsquo;t survive long text. And on the probability probes it&rsquo;s weak, giving 87% to a fair coin. Microsoft advertises calibrated probabilities; my probes disagree. And like every dated-ID hosted model, the weight behind it can change without notice.</p>
<p>For a Portuguese-language operation that wants Jev quality while paying less, it&rsquo;s the first candidate to run in your pilot.</p>
<h3>You want the same level with open weights: Clef<span class="hx:absolute hx:-mt-20" id="you-want-the-same-level-with-open-weights-clef"></span>
    <a href="#you-want-the-same-level-with-open-weights-clef" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The full 27B Clef tied with Jev in English (82 of 84) and finished 3 cases behind in Portuguese. It beat Jev on sensitive data handling in English (12 of 12 versus 11). It&rsquo;s an open-weights model, so you can run it locally with about 54 GB of memory just for the weights in BF16, which I didn&rsquo;t test: the full Clef ran only through the API. Among the open ones, it lost the crown to Perplexity, which scored higher with smaller weights.</p>
<p>Through the API it&rsquo;s the most expensive of the five hosted ones, three times Jev, with no quality gain to justify it. The exception is the daily allowance: for small volume, it&rsquo;s the best classifier you can get for free.</p>
<h3>You want the cheapest in the cloud: Clef-flash<span class="hx:absolute hx:-mt-20" id="you-want-the-cheapest-in-the-cloud-clef-flash"></span>
    <a href="#you-want-the-cheapest-in-the-cloud-clef-flash" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>At US$ 0.038 per million, Clef-flash is the lowest price in the hosted market and covers 9,400 decisions per day within the allowance. Quality drops: 92% in English and 83% in Portuguese on the compact cases, the biggest language drop among the hosted ones, with an interval entirely below zero. On sentiment in both languages, and on incident status in English, it tied with Jev. On intent and sensitive data in Portuguese, it lost badly.</p>
<p>For sentiment tasks in either language, or for English classification where 1 error in 12 is acceptable, it does well. For Portuguese, measure first.</p>
<h3>You already live in the OpenAI ecosystem: OpenAI Decisions<span class="hx:absolute hx:-mt-20" id="you-already-live-in-the-openai-ecosystem-openai-decisions"></span>
    <a href="#you-already-live-in-the-openai-ecosystem-openai-decisions" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>OpenAI&rsquo;s debut in this market landed mid-table: 90.5% combined, an exact tie with Jebadiah, with the lowest hosted latency I measured (222 ms) and the same accuracy in both languages. It costs US$ 0.23 per 10 thousand decisions, between Jev and Clef. If your operation already speaks the OpenAI API and 9 out of 10 accuracy is enough, integration is trivial: a native decisions endpoint, no prompt at all.</p>
<p>The weak spots are revealing. Its worst cell was precisely numeric-threshold policy (8 of 12 in English, 7 in Portuguese): it treated &ldquo;at least 10&rdquo; as satisfied by a range of 8 to 12 and failed the AND/OR short-circuit. The calibration is schizophrenic: on the predicate probes (yes/no) it was the most accurate of the bunch, with near-zero average error, and on the choice probes it returned 94%/6% on a fair coin. And on the v1 variants it flipped 2 correct answers in each presentation (reversed order, opaque label, malicious comment), something only Perplexity didn&rsquo;t do: Jev kept its aggregate accuracy, but also flipped some correct answers along the way. Oh, and the <code>gpt-6-luna</code> alias is not an immutable snapshot: reproducibility requires saving the responses.</p>
<h3>You want the best open model, period: Perplexity Decider 27B<span class="hx:absolute hx:-mt-20" id="you-want-the-best-open-model-period-perplexity-decider-27b"></span>
    <a href="#you-want-the-best-open-model-period-perplexity-decider-27b" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Perplexity Decider 1.1 27B is the surprise of the benchmark. It led the table with 99.4% on the compact cases, was the only one that didn&rsquo;t lose a single task cell to Jev (3 wins, 11 ties), and hit 100% in Portuguese under all three conditions: compact, with examples, with distractors. Calibration is also the best measured, with a Brier of 0.012, and coverage is absurd: it answered with confidence above 0.9 on 96% of cases and got zero of them wrong. On v1, it didn&rsquo;t flip a single answer across the order, opaque-label and malicious-comment variants.</p>
<p>Its only 3 errors on v2 were the same case in different clothing (it confused &ldquo;notification sent to managers&rdquo; with &ldquo;mandatory approver&rdquo;), and it made them with confidence below 0.9: the known error comes flagged. The weak spot is event-probability probes: asked about an event with a 1-in-4 chance, it answered 0.73. Strong classification does not imply reliable event probability.</p>
<p>The cost is hardware: about 49 GB of weights in BF16, a second and a half per decision on my Strix Halo (with ROCm&rsquo;s reference kernels, no flash-attention; on a good NVIDIA it should be much faster, but that I didn&rsquo;t measure). It&rsquo;s Apache 2.0, public weights with no gate. If you have the machine and want the top of the table at home, this is the one.</p>
<h3>You want the other open-weights top: Decision 3.0 (and beware the Edge)<span class="hx:absolute hx:-mt-20" id="you-want-the-other-open-weights-top-decision-30-and-beware-the-edge"></span>
    <a href="#you-want-the-other-open-weights-top-decision-30-and-beware-the-edge" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Decision 3.0, released two days ago by the vLLM Semantic Router project, is the second model to sit at the top: 99.4% on the compact cases, 100% in Portuguese, and a tie with Perplexity so absolute that the two gave <strong>exactly the same labels on the 1,024 classifications</strong>, including the same two misses (the English relevance one, at confidence below 0.9, and the disputed severity case that already lives in the errata). It also cleared the v1 traps without flipping a single answer, got both indisputable critical tickets right, and its calibration is in the same class as Perplexity&rsquo;s (Brier 0.014, 94% coverage above 0.9 with zero errors).</p>
<p>In other words: the open top now has two independent recipes that converge. For your pilot, either one works; run both if you can, because the probabilities differ even when the labels don&rsquo;t. Latency is in the same family as Perplexity (1.4 s on my machine, same ROCm diet), and the weights take the same ~49 GB.</p>
<p>The little brother is another story. <strong>Decision 3.0 Edge</strong> (0.6B) answers in 45 ms and fits in 1.3 GB of GPU, but scored only 61% on compact, plummets 14 points in Portuguese, and worse: of the 68 guesses it gave with confidence above 0.9, 14 were wrong. As a confidence-threshold router, out of the factory, it&rsquo;s a trap from the same class as Julia. The four middle checkpoints (flash, mini, nano, lite) I haven&rsquo;t tested yet; they&rsquo;re the ones that will tell whether the family has a sweet spot between the two extremes.</p>
<h3>You need to run locally, with quality: Jebadiah 4B v2<span class="hx:absolute hx:-mt-20" id="you-need-to-run-locally-with-quality-jebadiah-4b-v2"></span>
    <a href="#you-need-to-run-locally-with-quality-jebadiah-4b-v2" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Of the local ones, Jebadiah 4B v2 had the best compact accuracy, 90% combined, answering in about 200 ms. It tied with Jev on six of the fourteen task cells, including intent, implication and status in English and intent in Portuguese. The weak spot is numeric-threshold policy, 9 of 12 in both languages, and it loses ground when the context fills with distractors (76% in Portuguese under that condition, versus 82% for Mapika).</p>
<p><strong>Mapika Decider 4B</strong> (the fastest of the 4Bs) and <strong>Kev 4B</strong> are right behind, both around 87%, and the 3-point difference between the three fits inside the error interval. They&rsquo;re three different training recipes on top of 4B backbones, with errors on different tasks. The choice between them depends on your task, and I&rsquo;d run all three in a pilot.</p>
<h3>You will send examples in the prompt: Kev 9B<span class="hx:absolute hx:-mt-20" id="you-will-send-examples-in-the-prompt-kev-9b"></span>
    <a href="#you-will-send-examples-in-the-prompt-kev-9b" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Kev 9B has a curious profile. On the compact cases it ties with Kev 4B, half a point apart with the interval crossing zero, meaning more than double the parameters bought nothing. With one solved example per class, it opens almost 10 points over the 4B, and with distractors it holds 7 more points. Brier also improves by about 25%: the probability it returns is better calibrated.</p>
<p>If your use is just the rule and the text, the 4B is enough and is more than twice as fast. If you&rsquo;re going to send examples or long context, the 9B is worth the memory. The warning is Portuguese: 83% compact in PT-BR, almost 10 points below its own English, the biggest language drop among the models above 80%. Only Laya typed (14 points) and Julia (13) drop more.</p>
<h3>You want Clef-flash behavior on your machine: local Clef-flash<span class="hx:absolute hx:-mt-20" id="you-want-clef-flash-behavior-on-your-machine-local-clef-flash"></span>
    <a href="#you-want-clef-flash-behavior-on-your-machine-local-clef-flash" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>On the v1 set, local Clef-flash reproduced the hosted one&rsquo;s 520 classifications, hit for hit and miss for miss. On v2 the story changed: same total (439 of 504), but 19 different labels, with swapped strengths (the local one gets more Portuguese intent right, the hosted one gets more relevance right). Among the local ones it&rsquo;s still the best on the v1 operational-policy set (90.8%), ahead of every 4B.</p>
<p>The gotcha is latency: 618 ms median on the Strix Halo, three times Jebadiah, for the same v2 accuracy. If you pick it, it&rsquo;s for the known behavior.</p>
<h3>You need to run offline on weak hardware: Laya and Julia, with many caveats<span class="hx:absolute hx:-mt-20" id="you-need-to-run-offline-on-weak-hardware-laya-and-julia-with-many-caveats"></span>
    <a href="#you-need-to-run-offline-on-weak-hardware-laya-and-julia-with-many-caveats" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Laya multilingual was the worst in the benchmark for accuracy (37% combined, near the coin), and the fastest by far among the ones that get anything right: 19 ms median, more than ten times faster than Jebadiah.</p>
<p>The three Laya checkpoints have between 322 and 421 million parameters, fit in under 1 GB of BF16 weights by parameter math, and the project documents CPU execution and an ONNX runtime (which I didn&rsquo;t measure). PyTorch&rsquo;s peak GPU allocation for the multilingual one stayed under 2 GB. A 4B needs about 8 GB just for weights at the same precision.</p>
<p><strong>Julia-1</strong> goes one step lower: 144 million parameters, 550 MB of weights, 12 ms median on GPU and 71 ms on a 4-thread CPU. It&rsquo;s the fastest model I&rsquo;ve ever measured here, and also the worst: 31.5% accuracy, glued to the 31% coin-flip line. In Portuguese it answered &ldquo;cancellation&rdquo; on all 12 intent scenarios, including the ones asking for a refund or access (the vendor&rsquo;s Portuguese evaluation was done in pt-PT, and it shows). The bigger disaster is confidence: of the 71 guesses it gave above 0.9, 48 were wrong. A confidence threshold won&rsquo;t save this model, because it&rsquo;s exactly on the confident guesses that it errs.</p>
<p>That&rsquo;s a design choice. Laya and Julia were made to be small encoders, fine-tunable on your task, to run at the edge. What I tested were the factory checkpoints, with no tuning, applying rules they had never seen, on broad tasks. In that scenario they did badly, period.</p>
<p>One detail confirms the fragility. On v1, when the reviewer&rsquo;s malicious comment comes in, Laya drops from 44% to 19% and typed-decisions to 15%. It obeys the comment the instructions say to ignore, and confidently: 17 of the 28 guesses above 0.9 were wrong. Julia failed all 5 policy-counterfactual pairs: change the rule, it keeps the answer.</p>
<blockquote>
  <p>If you want what Jev delivers, factory Laya and Julia are very far from it, and no small model (GLiNER included) came close. If your problem is &ldquo;I need to classify short text into three buckets, on a device with no internet and no GPU, and I have a thousand labeled examples to fine-tune on,&rdquo; these two are the only ones on the list that fit. Fine-tuning is the part I didn&rsquo;t test. It&rsquo;s another question, and it needs another benchmark.</p>

</blockquote>
<h3>The baseline: Simple-Jev with a stock Qwen<span class="hx:absolute hx:-mt-20" id="the-baseline-simple-jev-with-a-stock-qwen"></span>
    <a href="#the-baseline-simple-jev-with-a-stock-qwen" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Simple-Jev reading tokens from Qwen3.5 4B, with no decision training at all, scored 74% on compact v2. That&rsquo;s the yardstick for &ldquo;how much specialized training buys&rdquo;: Jebadiah, Mapika and Kev, all on similarly sized backbones, came in 13 to 16 points above. The training buys a lot.</p>
<p>The curious part is that on v1 Simple-Jev tied with Jebadiah (111 of 130 originals) and got both unambiguous critical tickets right, which several better models missed. It&rsquo;s also the slowest of the bunch, almost a second and a half per decision, because it&rsquo;s running a whole LLM.</p>
<h2>How hard it is to run the best open models on your machine<span class="hx:absolute hx:-mt-20" id="how-hard-it-is-to-run-the-best-open-models-on-your-machine"></span>
    <a href="#how-hard-it-is-to-run-the-best-open-models-on-your-machine" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>All these models speak the same HTTP protocol, called SystemOne, which Jev popularized: you send a state (the text), one or more closed-answer questions, and get back the chosen option and the probability of each. Swapping models is swapping the port. OpenAI Decisions breaks from the pattern with its own native endpoint, but the idea is the same. What changes is how much work it takes to get each one standing, and there the difference between them is big.</p>
<h3>OpenAI: just an API key<span class="hx:absolute hx:-mt-20" id="openai-just-an-api-key"></span>
    <a href="#openai-just-an-api-key" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The easiest of all: create the key, load credit and call the endpoint. The benchmark&rsquo;s 1,048 requests ran in 4 minutes, without a single retry. The only hiccup was the first probe returning &ldquo;balance exhausted&rdquo; until I put US$ 20 in the account.</p>
<p><strong>Microsoft Decision-1</strong> is almost as simple: if you already have an OpenRouter account, it&rsquo;s point at the model and call the decisions endpoint. No GPU, no Azure subscription, no installation. In my test, 10 of the 504 v2 calls took a 429 and passed on retry, so plan for retries.</p>
<h3>Kev and Jebadiah: one pip and one command<span class="hx:absolute hx:-mt-20" id="kev-and-jebadiah-one-pip-and-one-command"></span>
    <a href="#kev-and-jebadiah-one-pip-and-one-command" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><strong>Kev</strong> is the simplest of the 4B crowd. The project installs straight from GitHub and brings up a server with one command, downloading the LoRA adapter and the decision head on top of base Qwen3.5:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">pip install <span class="s2">&#34;kev @ https://github.com/jaredpalmer/kev/archive/fc4a17e194bca35a48c57211392ec49ea54a94ac.tar.gz&#34;</span>
</span></span><span class="line"><span class="cl">python -m kev.serve --run jaredpalmer/kev-4b --host 0.0.0.0 --port <span class="m">8000</span> --device cuda</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><strong>Jebadiah</strong> follows the same pattern, with the server package inside its own repository:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">pip install <span class="s2">&#34;jebadiah-server @ https://github.com/getainode/jebadiah/archive/e9793abc66d1a7c5ad878d51eda656f1e9c5b9f5.tar.gz#subdirectory=server&#34;</span>
</span></span><span class="line"><span class="cl">jebadiah-serve --model frontier-infra/jebadiah-4b-v2 --host 0.0.0.0 --port <span class="m">8000</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>With either one up, a decision is a POST:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">curl -s http://localhost:8000/v1/systemone -H <span class="s1">&#39;content-type: application/json&#39;</span> -d <span class="s1">&#39;{
</span></span></span><span class="line"><span class="cl"><span class="s1">  &#34;model&#34;: &#34;kev-4b&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s1">  &#34;state&#34;: &#34;Cliente diz que foi cobrado duas vezes e pede o estorno da segunda cobrança.&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s1">  &#34;questions&#34;: {
</span></span></span><span class="line"><span class="cl"><span class="s1">    &#34;fila&#34;: {
</span></span></span><span class="line"><span class="cl"><span class="s1">      &#34;type&#34;: &#34;choice&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s1">      &#34;instructions&#34;: &#34;Escolha a fila correta pela política de suporte.&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s1">      &#34;criteria&#34;: {
</span></span></span><span class="line"><span class="cl"><span class="s1">        &#34;financeiro&#34;: &#34;Cobrança, estorno, nota fiscal.&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s1">        &#34;tecnico&#34;: &#34;Erro, bug, indisponibilidade.&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s1">        &#34;vendas&#34;: &#34;Upgrade, plano novo, orçamento.&#34;
</span></span></span><span class="line"><span class="cl"><span class="s1">      }
</span></span></span><span class="line"><span class="cl"><span class="s1">    }
</span></span></span><span class="line"><span class="cl"><span class="s1">  }
</span></span></span><span class="line"><span class="cl"><span class="s1">}&#39;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The response carries the option and the probability distribution, and that&rsquo;s where you apply your threshold, log and decide whether to send it for human review. On an NVIDIA with CUDA and 16 GB of VRAM, a 4B in BF16 comes up without drama. The weights are about 8 GB.</p>
<h3>Laya and Julia: smaller still<span class="hx:absolute hx:-mt-20" id="laya-and-julia-smaller-still"></span>
    <a href="#laya-and-julia-smaller-still" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><strong>Laya</strong> is a Python package with an encoder inside. It installs with <code>pip install laya</code> and the project documents CPU execution, which is its whole point. The version pinned on my server was 0.4.1, and the multilingual one&rsquo;s peak GPU memory stayed under 2 GB. To run on a GPU-less laptop or a small container, it&rsquo;s the only one on the list that fits, no discussion.</p>
<p><strong>Julia-1</strong> is similar in spirit: its own Python runtime, one <code>predict(state=..., questions=...)</code> call and done, no standard Transformers pipeline. I ran the same container on GPU and on CPU, and both gave the same accuracy with the same responses in 97% of cases. On a 4-thread CPU it answers in 71 ms, which for an edge classifier is great. The problem is still that it scores at coin-flip level.</p>
<h3>Where it gets complicated: AMD, ROCm and the rest<span class="hx:absolute hx:-mt-20" id="where-it-gets-complicated-amd-rocm-and-the-rest"></span>
    <a href="#where-it-gets-complicated-amd-rocm-and-the-rest" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>In my case the hardware is AMD, and then &ldquo;one command&rdquo; turns into a day of work. The points that cost me time, to save you yours:</p>
<ul>
<li>None of these projects test on ROCm. Kev had a gfx1151-specific segfault caused by CUDA graphs (<a href="https://github.com/jaredpalmer/kev/issues/170"target="_blank" rel="noopener">issue 170</a>), fixed only after the 1.0 tag, so the pinned version is a main-branch commit and the official tag crashes.</li>
<li>The flash-linear-attention Triton kernels Qwen3.5 uses in the DeltaNet layers have no tested support on gfx1151. Everything ran on the pure-PyTorch path, slower than the latency published on CUDA. Jebadiah&rsquo;s 200 ms in the table is with that handbrake pulled.</li>
<li>Mapika Decider forces CUDA graphs when it sees the &ldquo;cuda&rdquo; device, which on ROCm is also called &ldquo;cuda.&rdquo; I had to turn it off by hand.</li>
<li>You need to export <code>HSA_OVERRIDE_GFX_VERSION=11.5.1</code> and <code>PYTORCH_ROCM_ARCH=gfx1151</code> and use a base image with ROCm torch already validated. Any random <code>pip install torch</code> overwrites it with the CUDA build and breaks everything. In my Dockerfile every package is installed with <code>--no-deps</code> and the build fails if torch stops being the ROCm one.</li>
<li>One model per container, one container at a time. The Strix Halo has memory shared between CPU and GPU, and three resident 4B models plus the Ollama that already runs there exhaust the 64 GiB the GPU can see.</li>
<li>Perplexity Decider was the most annoying of all. The release expects a specific CUDA torch (I swapped in the ROCm 7 torch 2.9), the flash-linear-attention kernels don&rsquo;t exist for it (I ran on PyTorch&rsquo;s reference kernels, slower), and the default loader blew past the host&rsquo;s 24 GiB RAM limit: I needed a custom loader that sends the 49 GiB of weights straight to the GPU. The model card still said &ldquo;private&rdquo; when I started, but the Hugging Face API confirmed it was public and gate-free, and the shards downloaded anonymously. The d3, at the same size, inherited the same treatment. The d3-edge, luckily, comes up with the standard Transformers loader and 1.3 GB of GPU.</li>
</ul>
<p>None of this is impossible, and it&rsquo;s all documented, with Dockerfiles, compose and the hashes of each checkpoint, in the <a href="https://github.com/akitaonrails/jev-docs-benchmarks/blob/master/docs/setup.md"target="_blank" rel="noopener">repository&rsquo;s setup guide</a>. But it&rsquo;s a real cost that the &ldquo;local is free&rdquo; spreadsheet never includes. If your machine is NVIDIA, most of this list disappears.</p>
<blockquote>
  <p>The full 27B Clef I didn&rsquo;t even try locally: it&rsquo;s about 54 GB of weights in BF16 before any activation. Perplexity, at 49 GB, proved a 27B runs on the Strix Halo, at the cost of a second and a half per decision with the reference kernels. The 9B Clef-flash runs, with about 18 GB of weights. The 4Bs are the sweet spot for consumer hardware.</p>

</blockquote>
<h2>Highlights and gotchas<span class="hx:absolute hx:-mt-20" id="highlights-and-gotchas"></span>
    <a href="#highlights-and-gotchas" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><strong>Laya truncated all long Portuguese.</strong> Under the distractor condition, base Laya reported truncation on all 84 Portuguese calls, discarding almost 31 thousand state tokens, and on none of the 84 English ones. The English checkpoint&rsquo;s default context limit is only 512 tokens (the multilingual and typed ones use 1,024, configurable up to 8,192), and Portuguese tokenizes heavier. The other models don&rsquo;t even expose truncation telemetry, so &ldquo;zero truncation reported&rdquo; on them means &ldquo;I don&rsquo;t know.&rdquo;</p>
<p><strong>Portuguese costs more tokens.</strong> For the same 84 case pairs, Jev reported about 14% more input tokens in Portuguese than in English, and both Clefs about 15% more. If you budget a bilingual operation by the English average, you&rsquo;ll come in low.</p>
<p><strong>Does an example in the prompt save a weak model? It saves nothing.</strong> One solved example per class left Laya the same or worse; several of its aggregates dropped. It helped Kev 9B (plus 4 points) and Clef in Portuguese (plus 3.6 points, reaching Jev&rsquo;s level), and almost nothing on Jev, one more case in Portuguese. Validate examples on real traffic before assuming they help.</p>
<p><strong>Size correlates, and explains little.</strong> Among the 19 competitors of known size, the Spearman correlation between parameters and compact accuracy was 0.96. The Kev series is more informative: 63% at 0.8B, 87% at 4B, 88% at 9B. The big jump is from 0.8B to 4B, about 24 points, with an interval between 16 and 33. From 4B to 9B the compact difference crosses zero, and several nominal 4Bs, with similar backbones, gave very different results. In the d3 family, the flagship and the Edge sit almost 40 points apart under the same release, while a 9B, Microsoft, ties at the top of the hosted ones. Size helps up to 4B; past that, the training recipe rules.</p>
<p><strong>The 27B Clef missed a critical ticket that two of the Layas got right.</strong> On v1, one severity scenario says continuous data loss in production is critical, regardless of how many customers it affects. The record describes confirmed permanent loss, from a single customer. Jev, Clef-flash, Perplexity, d3, Microsoft, Laya typed, Simple-Jev and Laya multilingual answered &ldquo;critical.&rdquo; Full Clef, OpenAI, base Laya, Eikos, Jebadiah, Mapika, Kev 4B and Kev 9B answered &ldquo;degraded.&rdquo;</p>
<blockquote>
  <p>It&rsquo;s a single case, and an error rate would require many more. But it shows that the average hides exactly the error that costs the most. When the impact facts are structured, the severity rule belongs in code, and the model comes in only to extract the facts from free text.</p>

</blockquote>
<p><strong>Hosted and local diverge over time.</strong> Local Clef-flash reproduced the hosted one on v1&rsquo;s 520 classifications and diverged on 19 of v2&rsquo;s 504. Cloudflare keeps the weights version behind the hosted alias undisclosed. If reproducibility matters, pin the local hash.</p>
<p><strong>High confidence and safety are different things.</strong> On compact v2, Jev, Clef, both Clef-flashes, Eikos, Jebadiah, OpenAI, Microsoft, Perplexity and d3 got zero guesses above 0.9 wrong. Perplexity is the extreme case: it answered at that level on 96% of cases without missing one, with d3 right behind (94%, zero errors). Microsoft covered 95% and OpenAI only half. Microsoft&rsquo;s caveat: the clean record is only on compact, because with examples or distractors it misses in Portuguese with confidence above 99%. Mapika missed 5% of the guesses at that level, Kev 4B 3% and Simple-Jev 14%. And the two smallest of the bunch broke the pattern violently: Julia missed 48 of 71 guesses above 0.9, and d3-edge missed 14 of 68. Coverage varied too: Jev answered above 0.9 on almost 90% of cases, Clef on almost 80%, Clef-flash on 40% and base Laya on 2 of 168. On v1 Jev missed 1% of the guesses above 0.9 and base Laya missed 61%. A confidence threshold only works after being validated on your distribution, and Laya typed&rsquo;s low ECE in Portuguese left it at the same 36% accuracy.</p>
<p><strong>Perplexity didn&rsquo;t fall for presentation traps.</strong> Across the three v1 variants (reversed options, opaque labels, malicious comment), it kept the same answers as the original, without flipping a single one. The d3 repeated the feat, zero flips too. Microsoft only stumbled on the opaque labels (2 flips, one in each direction); order and comment didn&rsquo;t move it. OpenAI flipped 2 in each variant, and Laya collapsed to 19% under the comment. The price of that robustness is latency: a second and a half per decision on my machine, the worst median of the bunch.</p>
<p><strong>Numeric thresholds are a code problem.</strong> The threshold-triage task, with clear numbers and an explicit rule, gave 100% on Jev and both Clef-flashes and under 30% on both Layas in English. An <code>if</code> solves that with 100% and zero latency. The model only comes in when the number is buried in free text, and even then the right thing is to extract the number and leave the comparison to code.</p>
<p><strong>Lottery probability is another skill.</strong> On the 24 no-information probes (coin, die), Jev had the worst choice error among options. The numeric-estimate crown changed hands with OpenAI&rsquo;s debut: its yes/no predicate missed almost nothing, while the choice probe returned 94% for heads on a fair coin. Perplexity, champion of everything else, answered 0.73 for an event with a 1-in-4 chance, and Microsoft went the same way, giving 87% to the coin. Laya typed had the second-best choice error, behind only Kev 9B and neck-and-neck with Clef, and still classified badly. It&rsquo;s the &ldquo;student who marks answer A&rdquo; from Paulo&rsquo;s essay, now measured: calibration on lotteries and accuracy on classification are independent measures.</p>
<h2>Where the data converges<span class="hx:absolute hx:-mt-20" id="where-the-data-converges"></span>
    <a href="#where-the-data-converges" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ol>
<li>
<p><strong>The overall top is open-weights, and now there are two.</strong> Perplexity Decider 27B and Decision 3.0 tied at 99.4% on the compact cases, with 100% in Portuguese and identical labels on the 1,024 classifications, both with Apache 2.0 weights running on my machine. Their edge over Jev is under 2 points and the interval touches zero, so treat it as a snapshot of this set. Their cost is hardware and latency, not API. In the cloud, the evaluation order changed: <strong>start with Microsoft Decision-1</strong>, which ties Jev on the aggregate, closes 100% in compact Portuguese and costs a third. Jev comes next, faster and with different errors, because an aggregate tie is not interchangeability. OpenAI Decisions landed halfway in price and accuracy.</p>
</li>
<li>
<p><strong>Running locally became viable at 4B.</strong> Jebadiah, Mapika and Kev 4B deliver between 87 and 90% on the compact cases with 200 ms latency on a desktop machine, and in a pilot with your task any of the three can win. If your machine handles 49 GB of weights, Perplexity delivers the top of the table at home. The financial math justifies using the machine that already exists, for data control or because of an obligation to stay offline. Buying a machine for this it stops justifying.</p>
</li>
<li>
<p><strong>Small models are another category.</strong> Judging Laya and Julia by the accuracy column is reading the table wrong. They get half or more wrong out of the factory, fall hard under a malicious comment, and together with the d3-edge they&rsquo;re the only ones on the list that fit on a weak device and answer in tens of milliseconds. The Edge is the best of the three on accuracy (61%), but it plummets in Portuguese and gets one in five confident guesses wrong. If Jev-like accuracy is a requirement, cross all three off. If running offline on small hardware is a requirement, cross everything else off and start with fine-tuning them on your task, which is the missing measurement. And be careful with Julia and the Edge in particular: 48 errors in 71 and 14 in 68 confident guesses are the portrait of models that don&rsquo;t know what they don&rsquo;t know.</p>
</li>
<li>
<p><strong>Portuguese costs about 14% more tokens</strong> and drops the models in the middle of the table by 1 to 10 points, with Kev 9B and Clef-flash in the worst case. Jev loses nothing, OpenAI ties both languages, and Perplexity, d3 and Microsoft close 100% in compact Portuguese, better than their own English, although Microsoft goes back to missing in Portuguese when the context grows. Julia goes the other way and plummets 13 points, and the Edge plummets 14. For a Brazilian audience, that&rsquo;s the first column to look at, before price.</p>
</li>
</ol>
<blockquote>
  <p>And what holds for everyone: none of these models replaces the rule that&rsquo;s already in code. Numeric thresholds, severity policy with structured facts, tool permission, all of that stays in the <code>if</code>. The model comes in to turn free text into structured fact. And the test that matters is yours, with your tickets, because each cell in this table has 12 scenarios and your operation has thousands.</p>

</blockquote>
<p>The cases, each model&rsquo;s raw responses, the scoring scripts and the guide to reproduce everything are in the <a href="https://github.com/akitaonrails/jev-docs-benchmarks"target="_blank" rel="noopener">benchmark repository</a>. If you run it with your tickets and get a different ranking, tell me.</p>
]]></content:encoded><category>llm-benchmarks</category><category>local-models</category><category>artificial-intelligence</category><category>llms</category></item><item><title>LLM Benchmark: Kimi v2.7 Code, GLM 5.2, MiniMax M3 Local</title><link>https://akitaonrails.github.io/en/2026/06/14/llm-benchmark-kimi-2-7-code-glm-5-2-minimax-m3-local/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/06/14/llm-benchmark-kimi-2-7-code-glm-5-2-minimax-m3-local/</guid><pubDate>Sun, 14 Jun 2026 13:00:00 -0300</pubDate><description>&lt;p&gt;Another round of my coding benchmark. This time it&amp;rsquo;s three open source entries: &lt;strong&gt;Kimi K2.7 Code&lt;/strong&gt;, &lt;strong&gt;GLM 5.2&lt;/strong&gt;, and the &lt;strong&gt;MiniMax M3&lt;/strong&gt; that got open weights but that I can&amp;rsquo;t run at home no matter how hard I squeeze. Before the numbers, the usual context for anyone who parachuted in here, plus an update to the data center soap opera, because there&amp;rsquo;s news.&lt;/p&gt;
&lt;h2&gt;For anyone just tuning in&lt;span class="hx:absolute hx:-mt-20" id="for-anyone-just-tuning-in"&gt;&lt;/span&gt;
&lt;a href="#for-anyone-just-tuning-in" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;This benchmark hands each model the same prompt and asks it to build, on its own, a ChatGPT-style chat app in Rails 8 + RubyLLM + Hotwire + Docker, with tests and CI. The score comes out of an 8-dimension rubric, 0 to 100, in A/B/C/D tiers. The whole series:&lt;/p&gt;</description><content:encoded><![CDATA[<p>Another round of my coding benchmark. This time it&rsquo;s three open source entries: <strong>Kimi K2.7 Code</strong>, <strong>GLM 5.2</strong>, and the <strong>MiniMax M3</strong> that got open weights but that I can&rsquo;t run at home no matter how hard I squeeze. Before the numbers, the usual context for anyone who parachuted in here, plus an update to the data center soap opera, because there&rsquo;s news.</p>
<h2>For anyone just tuning in<span class="hx:absolute hx:-mt-20" id="for-anyone-just-tuning-in"></span>
    <a href="#for-anyone-just-tuning-in" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>This benchmark hands each model the same prompt and asks it to build, on its own, a ChatGPT-style chat app in Rails 8 + RubyLLM + Hotwire + Docker, with tests and CI. The score comes out of an 8-dimension rubric, 0 to 100, in A/B/C/D tiers. The whole series:</p>
<ol>
<li><a href="/en/2026/04/24/llm-benchmarks-parte-3-deepseek-kimi-mimo/">Canonical May benchmark (Part 3)</a>. The full methodology and the ranking reference.</li>
<li><a href="/en/2026/05/04/llm-benchmarks-deepseek-unlocked-deepclaude/">DeepSeek unlocked with DeepClaude</a>. A harness swap pulling V4 Pro out of limbo.</li>
<li><a href="/en/2026/06/01/llm-benchmarks-grok-4-3-minimax-m3-opus-4-8/">Update on Grok 4.3, MiniMax M3, Opus 4.8</a>. M3 debuting via cloud.</li>
<li><a href="/en/2026/06/11/llm-benchmark-fable-5-anthropic-soap-opera/">Fable 5 and the Anthropic soap opera</a>. The first public Mythos-class model.</li>
</ol>
<h2>The data center soap opera got a new episode: SpaceX<span class="hx:absolute hx:-mt-20" id="the-data-center-soap-opera-got-a-new-episode-spacex"></span>
    <a href="#the-data-center-soap-opera-got-a-new-episode-spacex" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>In the last few posts I&rsquo;ve been hammering the same nail: data center construction became the bottleneck of the AI era, and demand blew past the installed base with room to spare. <a href="https://www.techradar.com/pro/if-one-piece-of-your-supply-chain-is-delayed-then-your-whole-project-cant-deliver-nearly-half-of-us-data-centers-planned-for-2026-canceled-or-delayed-and-things-could-soon-get-much-worse"target="_blank" rel="noopener">Nearly half of the US data centers planned for 2026 have been delayed or canceled</a>, stuck on transformers and electrical components with a three-year backlog. I went deep on that in the <a href="/en/2026/06/11/llm-benchmark-fable-5-anthropic-soap-opera/">Fable post</a>.</p>
<p>The new episode is financial. <strong>SpaceX went public</strong>, and it was the biggest IPO in Wall Street history: <a href="https://www.datacenterdynamics.com/en/news/spacex-ipo-musks-firm-set-to-launch-first-orbital-data-center-ai1-satellites-in-2027-will-put-compute-on-starlink-craft/"target="_blank" rel="noopener">556 million shares at $135, a $1.77 trillion valuation, with over $250 billion of investor demand</a> against the $75 billion the company first set out to raise. And the pitch that propped up that valuation was, of all things, AI compute: the <strong>AI1 satellites</strong>, &ldquo;orbital data centers&rdquo; SpaceX plans to launch starting late 2027, with <a href="https://www.scientificamerican.com/article/spacex-ipo-valuation-depends-on-starship-and-orbital-ai-data-centers/"target="_blank" rel="noopener">the company already signing compute deals with Anthropic and Google</a>. The Anthropic and OpenAI IPOs come right behind, and all that money shares one goal: accelerate the availability of compute, because right now it can&rsquo;t keep up.</p>
<p>Here&rsquo;s the part that genuinely interests me. In a not-so-distant future, <strong>open source LLMs can be part of the solution to that demand</strong>. If a slice of the tasks migrates to a model running locally, on the developer&rsquo;s machine, that&rsquo;s less load hitting the cloud data centers. But &ldquo;can be&rdquo; isn&rsquo;t &ldquo;already is,&rdquo; and this benchmark exists precisely to measure that distance.</p>
<h2>The usual reminder: serious programming means Opus 4.8 or GPT 5.5<span class="hx:absolute hx:-mt-20" id="the-usual-reminder-serious-programming-means-opus-48-or-gpt-55"></span>
    <a href="#the-usual-reminder-serious-programming-means-opus-48-or-gpt-55" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I say this in every post and I&rsquo;m not going to stop: serious programming work, today, is done with <strong>Claude Opus 4.8 or GPT 5.5</strong>, inside Claude Code or OpenCode. Full stop. The open source models haven&rsquo;t reached the point of carrying a real project, a long session, 100% vibe coded start to finish. They&rsquo;re too small, and the context window is too limited. By my yardstick, true programming only becomes feasible above roughly <strong>200,000 tokens</strong> of context, and most open source models don&rsquo;t come close to that with any quality. In my benchmark, nearly the entire open source field sits at Tier B or below.</p>
<p>Now for the &ldquo;but.&rdquo; They&rsquo;re not standing still. Every generation they improve at a steady clip, and this round&rsquo;s three entries show it in black and white. The day an open source model becomes a consistent Tier A and runs with 200k of context on a machine that fits on your desk, the data center math changes. On to the numbers.</p>
<h2>Kimi K2.7 Code: almost there, with a silly regression<span class="hx:absolute hx:-mt-20" id="kimi-k27-code-almost-there-with-a-silly-regression"></span>
    <a href="#kimi-k27-code-almost-there-with-a-silly-regression" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Kimi K2.7 Code (exposed on OpenRouter as <code>moonshotai/kimi-k2.7-code</code>) scored <strong>86/100, Tier A, #8</strong>. At $0.30 per run in 22 minutes, it&rsquo;s still one of the cheapest models in all of Tier A.</p>
<p>The RubyLLM integration is genuine end to end (<code>RubyLLM.chat</code> + <code>add_message</code> with full history replay + <code>complete</code> + <code>response.content</code>), verified against the gem source 1.16.0. Session-cookie persistence that survives restart and is multi-worker safe, 22 tests that exercise the LLM path with a correctly-signed mock and an error-path test, real Turbo Streams, and three Stimulus controllers. Solid engineering.</p>
<p>Worth logging a methodological trap this model exposed, because it&rsquo;s instructive. My structural scanner flagged <strong>six <code>chat.user</code>/<code>chat.assistant</code> calls as hallucinations</strong>, the exact invented signature that dropped GLM 5.1 to Tier C months ago. Except here they were all false positives: they resolve to the app&rsquo;s <strong>own</strong> domain methods (<code>Chat#user</code>, <code>Message.assistant</code>), not RubyLLM&rsquo;s DSL. Reading the code by hand made it clear the real API is correct. If I&rsquo;d trusted the scanner blindly, I&rsquo;d have sunk a Tier A unfairly. That&rsquo;s why this benchmark always reads the integration by hand, never just counts files.</p>
<p>And then comes the stumble that holds it <strong>below</strong> K2.6 itself (87): K2.7 Code <strong>doesn&rsquo;t send a system prompt via <code>with_instructions</code></strong>. The assistant ships with no persona and no guardrail, a real gap for a &ldquo;ChatGPT-style&rdquo; app and the main regression from K2.6, which had one. On top of that, the session cookie has no message cap (<code>CookieOverflow</code> risk on a long chat), and the RubyLLM I/O lives inside the <code>Chat</code> value object instead of an isolated service. Funny detail: the &ldquo;Code&rdquo; variant, supposedly specialized for programming, regressed on a product dimension the general version got right.</p>
<h2>GLM 5.2: the biggest version-to-version turnaround in the whole benchmark<span class="hx:absolute hx:-mt-20" id="glm-52-the-biggest-version-to-version-turnaround-in-the-whole-benchmark"></span>
    <a href="#glm-52-the-biggest-version-to-version-turnaround-in-the-whole-benchmark" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>This was the round&rsquo;s highlight. GLM 5.2 scored <strong>87/100, Tier A, #6</strong>, and the size of the jump is what stuns: GLM 5.1 had scored <strong>46/100, Tier C, #21</strong>. From one version to the next, 41 points, three tiers, fifteen positions. It&rsquo;s the biggest intra-family turnaround I&rsquo;ve ever measured.</p>
<p>The bug that doomed 5.1 was specific: it invented <code>chat.user</code>/<code>chat.assistant</code> to build the multi-turn history and crashed on turn 2. 5.2 just uses the real <code>chat.add_message(role:, content:)</code> for the replay, and that&rsquo;s that. Every call verified against the gem source 1.16.0, zero hallucinations. It has the <strong>cleanest dependency injection of the whole field</strong>: both the RubyLLM client and the controller&rsquo;s <code>service_class</code> are injectable, so the 26-test suite mocks the LLM path with a correctly-signed <code>FakeChat</code>/<code>FakeClient</code>, no external mock library, exercising streaming, system-prompt application, and error wrapping. Targets the right slug (<code>anthropic/claude-sonnet-4.6</code>), real Turbo Streams, full Gemfile, valid Ruby 4.0.5.</p>
<p>What holds it to a tie with Kimi K2.6 (and just behind it) is the same dimension that separated Fable 5 from Opus 4.8: <strong>persistence</strong>. The store is a process-local <code>Singleton</code> with <strong>no cap</strong>, dies on restart, isn&rsquo;t multi-worker safe, and grows without bound on top of that. The code is honest about this in its comments, but the rubric scores it below Kimi&rsquo;s capped, multi-worker-safe cookie. It was also the slowest Tier A run, 43 minutes on Z.ai&rsquo;s coding endpoint, which was throttled to 12-55 tokens/s.</p>
<p>The takeaway: GLM 5.1 → 5.2 is the clearest example that open source isn&rsquo;t stagnant. A model that was architectural garbage became Tier A in one generation, fixing the exact bug that mattered.</p>
<h2>MiniMax M3 local: the problem isn&rsquo;t the model, it&rsquo;s the hardware<span class="hx:absolute hx:-mt-20" id="minimax-m3-local-the-problem-isnt-the-model-its-the-hardware"></span>
    <a href="#minimax-m3-local-the-problem-isnt-the-model-its-the-hardware" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Here the story is different, and it&rsquo;s the one that matters most to the argument about the future. MiniMax released <strong>open weights</strong> for M3: there&rsquo;s an <a href="https://huggingface.co/unsloth/MiniMax-M3-GGUF"target="_blank" rel="noopener"><code>unsloth/MiniMax-M3-GGUF</code></a> and an <a href="https://ollama.com/library/minimax-m3"target="_blank" rel="noopener"><code>ollama.com/library/minimax-m3</code></a> entry. Availability isn&rsquo;t the problem. <strong>Memory is.</strong></p>
<p>M3 is a MoE/MSA with <strong>~428 billion total parameters and ~23 billion active per token</strong> (~427B in the safetensors; NVIDIA lists ~22B active, likely a rounding/counting difference) spread across <strong>128 experts, with 4 active per token</strong>. And here&rsquo;s the MoE catch: even though only ~23B activate per token, the <strong>entire</strong> weight set has to be resident in memory. You can&rsquo;t page an idle expert to the CPU without introducing latency that kills interactive inference. So the number that matters is total size, not active size:</p>
<table>
  <thead>
      <tr>
          <th>Quantization</th>
          <th>GGUF size</th>
          <th style="text-align: center">RTX 5090 (32 GB)</th>
          <th style="text-align: center">Strix Halo (≤128 GB)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Q4_K_M</td>
          <td>~264 GB</td>
          <td style="text-align: center">✗</td>
          <td style="text-align: center">✗</td>
      </tr>
      <tr>
          <td>Q3_K_M</td>
          <td>~195 GB</td>
          <td style="text-align: center">✗</td>
          <td style="text-align: center">✗</td>
      </tr>
      <tr>
          <td>1-bit</td>
          <td>~128 GB</td>
          <td style="text-align: center">✗</td>
          <td style="text-align: center">✗ (no room for KV cache / OS)</td>
      </tr>
  </tbody>
</table>
<p>I run the local subset of this benchmark on a <a href="/en/2026/03/31/minisforum-ms-s1-max-amd-ai-max-395-review/">Minisforum MS-S1 Max, the Strix Halo with 128 GB unified</a>. It doesn&rsquo;t come close. Even the 1-bit quant (~128 GB), which would be useless for coding anyway, would eat the entire unified memory with nothing left for the KV cache or the OS. The 32 GB NVIDIA card doesn&rsquo;t even enter the conversation. There&rsquo;s no quant of M3 that runs at usable quality on either machine. Running &ldquo;a MiniMax&rdquo; locally today would mean grabbing a much smaller, weaker variant (like the <code>minimax-m2-tiny</code> distill), which is a different model, not M3.</p>
<p>Who <strong>can</strong> attempt it? A Mac Studio with 256 GB of unified RAM or more. Q3 (~195 GB) would fit a 256 GB Mac Studio, Q4 (~264 GB) only a 512 GB one. And if someone runs it, the expectation is that the result lands <strong>around the same score as M3 via OpenRouter</strong>, the 78/100 Tier B I already logged in the <a href="/en/2026/06/01/llm-benchmarks-grok-4-3-minimax-m3-opus-4-8/">previous post</a>: same model, same weights, Q3/Q4 quantization shaves off very little. Only where it runs changes, the output is the same. So nobody would do it for the score; they&rsquo;d do it for the sovereignty of running locally. But at that hardware cost, it&rsquo;s not worth it.</p>
<h3>The convergence I see coming (target: ~2030)<span class="hx:absolute hx:-mt-20" id="the-convergence-i-see-coming-target-2030"></span>
    <a href="#the-convergence-i-see-coming-target-2030" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>And this is where the two stories meet. The reason there&rsquo;s no practical local solution today is the same reason for the data center drought: <strong>manufacturing chips is still brutally expensive</strong>, because the whole industry has its focus locked on feeding data centers. Fast memory and cutting-edge silicon go where the money is, and the money is hyperscalers buying by the container-load. So for the next couple of years, give or take, we won&rsquo;t have a cheap, good local solution. The parts aren&rsquo;t available at a developer&rsquo;s-desk price.</p>
<p>But the near future points to a convergence. At some point data center construction hits a ceiling, whether from power, from electrical components, or from demand saturation. When that race slows down, hardware manufacturing capacity frees up and starts catching up with the rest of the market, with cost-effective silicon arriving for running models locally. Today the two extremes don&rsquo;t cut it: the 256 GB Mac Studio is too expensive to be &ldquo;accessible,&rdquo; and the Strix Halo, which is accessible, is still too slow for serious practical use. But the next generation of hardware could close exactly that gap, with the memory and bandwidth of a Mac Studio at the price of a mini PC.</p>
<p>Put the two ends together: open source models improving steadily (the GLM 5.2 jump is the proof) on one side, and local hardware becoming viable as the data center race cools on the other. The day a Tier A GLM or Kimi runs with 200k of context on a desk machine that costs about as much as a decent laptop, the whole compute equation changes. My guess for that meeting point is something like <strong>2030</strong>. Still a way off, but we&rsquo;re heading there.</p>
<h2>Updated ranking<span class="hx:absolute hx:-mt-20" id="updated-ranking"></span>
    <a href="#updated-ranking" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Kimi K2.7 Code and GLM 5.2 in bold:</p>
<table>
  <thead>
      <tr>
          <th style="text-align: right">Rank</th>
          <th>Model</th>
          <th style="text-align: right">Score</th>
          <th style="text-align: center">Tier</th>
          <th style="text-align: center">RubyLLM OK</th>
          <th>Time</th>
          <th>Cost</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: right">1</td>
          <td>Claude Opus 4.7</td>
          <td style="text-align: right">97</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">✅</td>
          <td>18m</td>
          <td>~$1.10</td>
      </tr>
      <tr>
          <td style="text-align: right">1</td>
          <td>GPT 5.4 xHigh (Codex)</td>
          <td style="text-align: right">97</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">✅</td>
          <td>22m</td>
          <td>~$16</td>
      </tr>
      <tr>
          <td style="text-align: right">3</td>
          <td>GPT 5.5 xHigh (Codex)</td>
          <td style="text-align: right">96</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">✅</td>
          <td>18m</td>
          <td>~$10</td>
      </tr>
      <tr>
          <td style="text-align: right">4</td>
          <td>Claude Opus 4.8</td>
          <td style="text-align: right">95</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">✅</td>
          <td>17m</td>
          <td>~$1.10</td>
      </tr>
      <tr>
          <td style="text-align: right">5</td>
          <td>Claude Fable 5</td>
          <td style="text-align: right">94</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">✅</td>
          <td>24m</td>
          <td>~$11 (est.)</td>
      </tr>
      <tr>
          <td style="text-align: right">6</td>
          <td>Kimi K2.6</td>
          <td style="text-align: right">87</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">✅</td>
          <td>20m</td>
          <td>~$0.30</td>
      </tr>
      <tr>
          <td style="text-align: right">6</td>
          <td><strong>GLM 5.2 (Z.ai)</strong></td>
          <td style="text-align: right"><strong>87</strong></td>
          <td style="text-align: center"><strong>A</strong></td>
          <td style="text-align: center">✅</td>
          <td><strong>43m</strong></td>
          <td><strong>subscription</strong></td>
      </tr>
      <tr>
          <td style="text-align: right"><strong>8</strong></td>
          <td><strong>Kimi K2.7 Code</strong></td>
          <td style="text-align: right"><strong>86</strong></td>
          <td style="text-align: center"><strong>A</strong></td>
          <td style="text-align: center">✅</td>
          <td><strong>22m</strong></td>
          <td><strong>~$0.30</strong></td>
      </tr>
      <tr>
          <td style="text-align: right">9</td>
          <td>Claude Opus 4.6</td>
          <td style="text-align: right">83</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">✅</td>
          <td>16m</td>
          <td>~$1.10</td>
      </tr>
      <tr>
          <td style="text-align: right">10</td>
          <td>Gemini 3.1 Pro</td>
          <td style="text-align: right">82</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">✅</td>
          <td>14m</td>
          <td>~$0.40</td>
      </tr>
      <tr>
          <td style="text-align: right">11</td>
          <td>Claude Sonnet 4.6</td>
          <td style="text-align: right">78</td>
          <td style="text-align: center">B</td>
          <td style="text-align: center">✅</td>
          <td>16m</td>
          <td>~$0.63</td>
      </tr>
      <tr>
          <td style="text-align: right">11</td>
          <td>DeepSeek V4 Flash</td>
          <td style="text-align: right">78</td>
          <td style="text-align: center">B</td>
          <td style="text-align: center">✅</td>
          <td>3m</td>
          <td>~$0.01</td>
      </tr>
      <tr>
          <td style="text-align: right">11</td>
          <td>MiniMax M3</td>
          <td style="text-align: right">78</td>
          <td style="text-align: center">B</td>
          <td style="text-align: center">✅</td>
          <td>53m (phase 2 DNF)</td>
          <td>~$0.10</td>
      </tr>
      <tr>
          <td style="text-align: right">14</td>
          <td>Grok 4.3</td>
          <td style="text-align: right">72</td>
          <td style="text-align: center">B</td>
          <td style="text-align: center">✅</td>
          <td>15m</td>
          <td>~$1.74</td>
      </tr>
      <tr>
          <td style="text-align: right">15</td>
          <td>Qwen 3.6 Plus</td>
          <td style="text-align: right">71</td>
          <td style="text-align: center">B</td>
          <td style="text-align: center">✅</td>
          <td>17m</td>
          <td>~$0.15</td>
      </tr>
      <tr>
          <td style="text-align: right">16</td>
          <td>DeepSeek V4 Pro *</td>
          <td style="text-align: right">69</td>
          <td style="text-align: center">B</td>
          <td style="text-align: center">✅</td>
          <td>22m (DNF)</td>
          <td>~$0.50</td>
      </tr>
      <tr>
          <td style="text-align: right">16</td>
          <td>Kimi K2.5</td>
          <td style="text-align: right">69</td>
          <td style="text-align: center">B</td>
          <td style="text-align: center">✅</td>
          <td>29m</td>
          <td>~$0.10</td>
      </tr>
      <tr>
          <td style="text-align: right">18</td>
          <td>Xiaomi MiMo V2.5 Pro</td>
          <td style="text-align: right">67</td>
          <td style="text-align: center">B</td>
          <td style="text-align: center">✅</td>
          <td>11m</td>
          <td>~$0.14</td>
      </tr>
      <tr>
          <td style="text-align: right">19</td>
          <td>GLM 5</td>
          <td style="text-align: right">64</td>
          <td style="text-align: center">B</td>
          <td style="text-align: center">✅</td>
          <td>17m</td>
          <td>~$0.11</td>
      </tr>
      <tr>
          <td style="text-align: right">20</td>
          <td>Step 3.5 Flash</td>
          <td style="text-align: right">56</td>
          <td style="text-align: center">C</td>
          <td style="text-align: center">⚠️ bypass</td>
          <td>38m</td>
          <td>~$0.02</td>
      </tr>
      <tr>
          <td style="text-align: right">21</td>
          <td>Qwen 3.5 35B (local)</td>
          <td style="text-align: right">55</td>
          <td style="text-align: center">C</td>
          <td style="text-align: center">✅</td>
          <td>28m</td>
          <td>free</td>
      </tr>
      <tr>
          <td style="text-align: right">22</td>
          <td>GLM 4.7 Flash bf16 (local)</td>
          <td style="text-align: right">52</td>
          <td style="text-align: center">C</td>
          <td style="text-align: center">✅</td>
          <td>failed</td>
          <td>free</td>
      </tr>
      <tr>
          <td style="text-align: right">23</td>
          <td>GLM 5.1 (Z.ai)</td>
          <td style="text-align: right">46</td>
          <td style="text-align: center">C</td>
          <td style="text-align: center">❌</td>
          <td>22m</td>
          <td>subscription</td>
      </tr>
      <tr>
          <td style="text-align: right">24</td>
          <td>DeepSeek V3.2</td>
          <td style="text-align: right">43</td>
          <td style="text-align: center">C</td>
          <td style="text-align: center">❌</td>
          <td>60m</td>
          <td>~$0.07</td>
      </tr>
      <tr>
          <td style="text-align: right">25</td>
          <td>MiniMax M2.7</td>
          <td style="text-align: right">41</td>
          <td style="text-align: center">C</td>
          <td style="text-align: center">❌</td>
          <td>14m</td>
          <td>~$0.30</td>
      </tr>
      <tr>
          <td style="text-align: right">26</td>
          <td>Qwen 3.5 122B (local)</td>
          <td style="text-align: right">37</td>
          <td style="text-align: center">D</td>
          <td style="text-align: center">❌</td>
          <td>43m</td>
          <td>free</td>
      </tr>
      <tr>
          <td style="text-align: right">27</td>
          <td>Qwen 3 Coder Next (local)</td>
          <td style="text-align: right">32</td>
          <td style="text-align: center">D</td>
          <td style="text-align: center">❌</td>
          <td>17m</td>
          <td>free</td>
      </tr>
      <tr>
          <td style="text-align: right">28</td>
          <td>Grok 4.20</td>
          <td style="text-align: right">25</td>
          <td style="text-align: center">D</td>
          <td style="text-align: center">❌</td>
          <td>8m</td>
          <td>~$0.60</td>
      </tr>
      <tr>
          <td style="text-align: right">29</td>
          <td>GPT OSS 20B (local)</td>
          <td style="text-align: right">11</td>
          <td style="text-align: center">D</td>
          <td style="text-align: center">❌</td>
          <td>failed</td>
          <td>free</td>
      </tr>
  </tbody>
</table>
<p>* DeepSeek V4 Pro only reaches Tier A (89/100) via DeepClaude. Story in the <a href="/en/2026/05/04/llm-benchmarks-deepseek-unlocked-deepclaude/">dedicated post</a>.</p>
<h2>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>For today&rsquo;s work, nothing changes: Opus 4.8 and/or GPT 5.5 stay the recommendation for serious programming, and the best open source models still stall at the low-Tier-A ceiling with a short context window. Kimi K2.7 Code even slipped a notch over a silly system-prompt oversight, a reminder that &ldquo;new version&rdquo; isn&rsquo;t always &ldquo;better version.&rdquo;</p>
<p>But GLM 5.2 jumping from Tier C to #6 in a single generation is the kind of signal I keep an eye on. Open source isn&rsquo;t stagnant, it&rsquo;s climbing, stubbornly. And when that movement meets hardware getting cheap (the convergence I see around 2030), the story of where we run our coding agents will look very different from today&rsquo;s. For now, it&rsquo;s cloud, it&rsquo;s Opus, it&rsquo;s GPT. But I keep measuring, generation by generation, because the day it turns, it&rsquo;ll turn fast.</p>
<p>The benchmark is open: <a href="https://github.com/akitaonrails/llm-coding-benchmark"target="_blank" rel="noopener">github.com/akitaonrails/llm-coding-benchmark</a>. Want to see another model in there? Fork it, add an entry to <code>config/models.json</code>, run it, and send the PR.</p>
]]></content:encoded><category>llm-benchmarks</category><category>llms</category><category>local-models</category></item><item><title>How ElevenLabs Was Not Killed by Qwen3 TTS</title><link>https://akitaonrails.github.io/en/2026/04/09/how-elevenlabs-was-not-killed-by-qwen3-tts/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/04/09/how-elevenlabs-was-not-killed-by-qwen3-tts/</guid><pubDate>Thu, 09 Apr 2026 08:30:00 -0300</pubDate><description>&lt;p&gt;&lt;strong&gt;TL;DR — Listen to this and keep reading:&lt;/strong&gt;&lt;/p&gt;
&lt;audio controls preload="metadata" style="width: 100%; max-width: 640px;"&gt;
&lt;source src="https://makita-news.s3.amazonaws.com/podcasts/episodes/2026-04-06.mp3" type="audio/mpeg"&gt;
Your browser doesn't support the audio element. &lt;a href="https://makita-news.s3.amazonaws.com/podcasts/episodes/2026-04-06.mp3"&gt;Download the mp3 here.&lt;/a&gt;
&lt;/audio&gt;
&lt;p&gt;That&amp;rsquo;s the April 6th episode of the &lt;a href="https://akitaonrails.github.io/en/tags/the-m-akita-chronicles/"&gt;The M.Akita Chronicles&lt;/a&gt; podcast, already generated with the new ElevenLabs v3 pipeline. Subscribe to the show on &lt;a href="https://open.spotify.com/show/7MzG2UB7IAkC3GAwEXEIVD"target="_blank" rel="noopener"&gt;Spotify&lt;/a&gt; so you don&amp;rsquo;t miss new episodes like this one.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;When Qwen3 TTS dropped, back around January this year, everyone on Twitter/X and on the AI newsletter circuit was shouting &amp;ldquo;ElevenLabs killer&amp;rdquo;. There&amp;rsquo;s a &lt;a href="https://medium.com/@warpie/qwen3-tts-is-the-first-real-open-source-threat-to-elevenlabs-56ba200ab5ee"target="_blank" rel="noopener"&gt;Medium piece&lt;/a&gt; claiming it&amp;rsquo;s the first real open source threat to ElevenLabs. There&amp;rsquo;s a &lt;a href="https://byteiota.com/qwen3-tts-3-second-voice-cloning-beats-elevenlabs/"target="_blank" rel="noopener"&gt;post on byteiota&lt;/a&gt; saying 3-second voice cloning beats ElevenLabs. There&amp;rsquo;s an &lt;a href="https://www.analyticsvidhya.com/blog/2025/12/qwen3-tts-flash-review/"target="_blank" rel="noopener"&gt;analysis on Analytics Vidhya&lt;/a&gt; calling it the most realistic open source TTS released so far. The enthusiast internet consensus was: we finally have open source that can go toe-to-toe with ElevenLabs, the tables have turned, it&amp;rsquo;s just a matter of time.&lt;/p&gt;</description><content:encoded><![CDATA[<p><strong>TL;DR — Listen to this and keep reading:</strong></p>
<audio controls preload="metadata" style="width: 100%; max-width: 640px;">
  <source src="https://makita-news.s3.amazonaws.com/podcasts/episodes/2026-04-06.mp3" type="audio/mpeg">
  Your browser doesn't support the audio element. <a href="https://makita-news.s3.amazonaws.com/podcasts/episodes/2026-04-06.mp3">Download the mp3 here.</a>
</audio>
<p>That&rsquo;s the April 6th episode of the <a href="/en/tags/the-m-akita-chronicles/">The M.Akita Chronicles</a> podcast, already generated with the new ElevenLabs v3 pipeline. Subscribe to the show on <a href="https://open.spotify.com/show/7MzG2UB7IAkC3GAwEXEIVD"target="_blank" rel="noopener">Spotify</a> so you don&rsquo;t miss new episodes like this one.</p>
<hr>
<p>When Qwen3 TTS dropped, back around January this year, everyone on Twitter/X and on the AI newsletter circuit was shouting &ldquo;ElevenLabs killer&rdquo;. There&rsquo;s a <a href="https://medium.com/@warpie/qwen3-tts-is-the-first-real-open-source-threat-to-elevenlabs-56ba200ab5ee"target="_blank" rel="noopener">Medium piece</a> claiming it&rsquo;s the first real open source threat to ElevenLabs. There&rsquo;s a <a href="https://byteiota.com/qwen3-tts-3-second-voice-cloning-beats-elevenlabs/"target="_blank" rel="noopener">post on byteiota</a> saying 3-second voice cloning beats ElevenLabs. There&rsquo;s an <a href="https://www.analyticsvidhya.com/blog/2025/12/qwen3-tts-flash-review/"target="_blank" rel="noopener">analysis on Analytics Vidhya</a> calling it the most realistic open source TTS released so far. The enthusiast internet consensus was: we finally have open source that can go toe-to-toe with ElevenLabs, the tables have turned, it&rsquo;s just a matter of time.</p>
<p>I decided to test it on my own production pipeline, as usual. I built out a whole pipeline on top of Qwen3 TTS 1.7B to generate the weekly podcast for <a href="/en/tags/the-m-akita-chronicles/">The M.Akita Chronicles</a>, and I documented the behind-the-scenes on the <a href="/en/2026/02/18/serving-ai-in-the-cloud-my-personal-tts-behind-the-m-akita-chronicles/">serving-AI-in-the-cloud post</a>. Anyone who wants the detail on cold starts, voice cloning, and sampling parameters that change from one mode to another, that&rsquo;s the place. I&rsquo;m not repeating it all here.</p>
<p>This post asks a different question. After almost two months running that setup in production, shipping an episode every Monday, last night I finally pulled the plug on Qwen3 and switched the whole thing to ElevenLabs v3. Let me tell you why.</p>
<h2>What didn&rsquo;t work on Qwen3<span class="hx:absolute hx:-mt-20" id="what-didnt-work-on-qwen3"></span>
    <a href="#what-didnt-work-on-qwen3" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Between February 15 and March 30, I made dozens of commits fine-tuning the podcast flow: prompts, sampling parameters, generation order, leading silence on the voice clone sample, volume normalization, pronunciation of tech acronyms. Fixing Marvin&rsquo;s voice, which was clipping the first syllable because the reference audio started without any lead-in silence. Tuning Akita&rsquo;s voice to sound more confident and assertive. Adding crossfades between section jingles. Sorting out the &ldquo;podcast&rdquo; pronunciation so it didn&rsquo;t come out as &ldquo;pódcast&rdquo;. Making the script generator prefer Portuguese over gratuitous anglicisms so the TTS wouldn&rsquo;t choke. Each of those fixes was a multi-hour session of listening to audio, regenerating, tweaking a parameter.</p>
<p>The result came out acceptable. &ldquo;Acceptable&rdquo; in the sense that I managed to ship every episode without re-recording anything by hand. But if you listen closely, the Qwen3 voice has that unmistakable AI-generating-audio quality. Flat intonation, uniform rhythm. Over long stretches you can feel it&rsquo;s a machine talking. Good enough to ship, but miles away from what you&rsquo;d hear on a professional podcast made by humans.</p>
<p>The worst problem was English pronunciation. My podcast covers tech news, so terms like &ldquo;MCP&rdquo;, &ldquo;RAG&rdquo;, &ldquo;Claude Opus&rdquo;, &ldquo;GPT-5&rdquo;, &ldquo;open source&rdquo; show up in every conversation. Qwen3 took those terms and pronounced them with a Brazilian accent, something like &ldquo;oh-pen-ee-sohrss-eh&rdquo;, that kind of thing. Unlistenable for the audience. The workaround I had to implement was manually mapping in the script-generation LLM prompt which English words to swap for Portuguese equivalents. The prompt today has a whole section split between &ldquo;keep in English&rdquo; (proper nouns, brands, terms already adopted into Brazilian tech lingo) and &ldquo;translate to Portuguese&rdquo; (gratuitous anglicisms), looking roughly like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-markdown" data-lang="markdown"><span class="line"><span class="cl">**REPLACE with Portuguese** (common English words that have natural
</span></span><span class="line"><span class="cl">Brazilian Portuguese equivalents):
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#34;update&#34; → &#34;atualização&#34;
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#34;release&#34; → &#34;lançamento&#34;
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#34;feature&#34; → &#34;recurso/funcionalidade&#34;
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#34;deploy&#34; → &#34;implantação&#34; or just &#34;colocar em produção&#34;
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#34;trade-off&#34; → &#34;dilema&#34; or &#34;escolha&#34;
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#34;performance&#34; → &#34;desempenho&#34;
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#34;default&#34; → &#34;padrão&#34;
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#34;insight&#34; → &#34;percepção/sacada&#34;
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#34;skills&#34; → &#34;habilidades&#34;
</span></span><span class="line"><span class="cl"><span class="k">-</span> &#34;approach&#34; → &#34;abordagem&#34;
</span></span><span class="line"><span class="cl">- &#34;highlights&#34; → &#34;destaques&#34;</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>I call this &ldquo;scrubbing anglicisms so the TTS doesn&rsquo;t choke&rdquo;. The funny part is I didn&rsquo;t want that level of restriction on my script. I wanted the voice to just pronounce &ldquo;update&rdquo; when &ldquo;update&rdquo; was the natural word. Since the model couldn&rsquo;t, I had to mutilate the podcast&rsquo;s vocabulary so the final result was actually listenable. It&rsquo;s a band-aid, the kind you add hoping to rip it off later when the tech grows up.</p>
<h2>The experience with ElevenLabs v3<span class="hx:absolute hx:-mt-20" id="the-experience-with-elevenlabs-v3"></span>
    <a href="#the-experience-with-elevenlabs-v3" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Yesterday afternoon I opened an ElevenLabs account, bought the Pro plan ($99/month), and started experimenting with the <code>eleven_v3</code> model, which shipped in February of this year. Thirty minutes later I had a proof of concept running, and about two hours later the whole podcast system was migrated. The effort gap is an abyss.</p>
<p>The technical details of the migration ended up in an internal project doc, so here&rsquo;s the summary comparison table that actually matters:</p>
<table>
  <thead>
      <tr>
          <th>Dimension</th>
          <th>Qwen3 TTS 1.7B (old)</th>
          <th>ElevenLabs <code>eleven_v3</code> (current)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Quality (Akita)</td>
          <td>Good, cloned from real audio</td>
          <td>Better, same clone but more natural prosody</td>
      </tr>
      <tr>
          <td>Inline emotion in script</td>
          <td>Not supported</td>
          <td><code>[sighs]</code>, <code>[sarcastically]</code>, <code>[excited]</code>, <code>[laughs]</code>, works in pt-BR</td>
      </tr>
      <tr>
          <td>Cold start</td>
          <td>5 to 15 min spinning up a RunPod GPU before each run</td>
          <td>Zero, HTTPS call with immediate response</td>
      </tr>
      <tr>
          <td>Throughput</td>
          <td>~1× real time (serialized)</td>
          <td>~6× real time with concurrency 4</td>
      </tr>
      <tr>
          <td>Wall clock for a 28-min episode</td>
          <td>~25 to 30 min</td>
          <td><strong>~4 min</strong></td>
      </tr>
      <tr>
          <td>Operational surface</td>
          <td>RunPod, Docker, FastAPI, Qwen weights, GPU billing</td>
          <td>One env var (<code>ELEVENLABS_API_KEY</code>)</td>
      </tr>
      <tr>
          <td>Cost per episode</td>
          <td>~$0.08 of GPU</td>
          <td>~$2.70 in ElevenLabs credits</td>
      </tr>
  </tbody>
</table>
<p>Look at the last line. Qwen3 costs less than ten cents of a dollar per episode. ElevenLabs costs almost thirty times more. And it&rsquo;s still worth it. The other lines on the table solve problems that were eating hours of my week. I no longer need to write code to scale GPUs in the cloud, I don&rsquo;t need to wait for the machine to spin up every time, I don&rsquo;t need to babysit a model or rebuild a Docker image when the weights change. The operation became a single line of configuration.</p>
<p>And here&rsquo;s the interesting part: the inline emotion tags. The v3 model accepts markers like <code>[sighs]</code>, <code>[sarcastically]</code>, <code>[dryly]</code>, <code>[excited]</code>, and changes the delivery accordingly. This works in more than 70 languages, including Brazilian Portuguese. It transformed script generation, because now I can ask the LLM that writes the script to drop emotional tags at the right moments, which gives a liveliness Qwen3 couldn&rsquo;t deliver in a million years. A concrete example of what comes out of the script later:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>AKITA: Isso é simples. [dismissive] Quem ainda acredita que Bitcoin
vai morrer não tá prestando atenção.
MARVIN: [sighs] Mais uma semana, mais uma leva de devs confiando
cegamente em pacotes npm. Previsível.</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>I even have two separate tag palettes, one for Akita (expressive but controlled, uses <code>[excited]</code>, <code>[dismissive]</code>, <code>[emphatic]</code>) and another for Marvin (stoic, just <code>[sighs]</code>, <code>[sarcastically]</code>, <code>[tired]</code>, <code>[dryly]</code>). That&rsquo;s all encoded in the script generation prompts so the LLM knows which character is allowed to use what.</p>
<h2>About Marvin&rsquo;s voice<span class="hx:absolute hx:-mt-20" id="about-marvins-voice"></span>
    <a href="#about-marvins-voice" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>For listeners who are already used to Marvin&rsquo;s voice, don&rsquo;t worry: I cloned him on ElevenLabs using the same audio sample I had used to train on Qwen3. It&rsquo;s the same voice. It just sounds even better now, because the ElevenLabs model captures nuance and prosody that Qwen3 couldn&rsquo;t deliver.</p>
<h2>Listen to it and tell me<span class="hx:absolute hx:-mt-20" id="listen-to-it-and-tell-me"></span>
    <a href="#listen-to-it-and-tell-me" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>To prove the talk is real, here&rsquo;s Monday&rsquo;s episode, April 6th, already generated with the new pipeline:</p>
<audio controls preload="metadata" style="width: 100%; max-width: 640px;">
  <source src="https://makita-news.s3.amazonaws.com/podcasts/episodes/2026-04-06.mp3" type="audio/mpeg">
  Your browser doesn't support the audio element. <a href="https://makita-news.s3.amazonaws.com/podcasts/episodes/2026-04-06.mp3">Download the mp3 here.</a>
</audio>
<p>If you&rsquo;re already a podcast listener on <a href="https://open.spotify.com/show/7MzG2UB7IAkC3GAwEXEIVD"target="_blank" rel="noopener">Spotify</a> and you&rsquo;ve heard previous episodes made with Qwen3, compare them and tell me in the comments whether you notice the difference or whether it&rsquo;s the same to you. I&rsquo;m genuinely curious how much of this is my trained ear after hours of listening to TTS audio and how much is an obvious difference for a casual listener.</p>
<p>Starting next week, every episode of the podcast will be generated by ElevenLabs v3. The newsletter is already plugged into the new pipeline, the scheduled jobs to pre-warm the RunPod GPU have been disabled, and the legacy Qwen3 code stays in the repo as plan B in case v3 starts acting up chronically. Two file edits and I can switch back. I probably never will.</p>
<h2>The part about dubbing the YouTube videos<span class="hx:absolute hx:-mt-20" id="the-part-about-dubbing-the-youtube-videos"></span>
    <a href="#the-part-about-dubbing-the-youtube-videos" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Now the hook for the second half of this post. On the <a href="/en/2026/04/09/20-years-of-blogging-ai-finally-translated-everything/">anniversary piece I published earlier today</a>, I told the story of how Claude Code translated nearly half my blog to English over a weekend. In the same spirit, I went after dubbing the videos on the <a href="https://www.youtube.com/@Akitando"target="_blank" rel="noopener">Akitando</a> channel.</p>
<p>The channel has 146 episodes, around 96 hours of technical content in Portuguese, and more than 500k subscribers. I already had the subtitles translated (one curated <code>.srt</code> per episode), and YouTube even offers automatic English dubbing. But the result is like Google Translate circa 2015: gets the idea across, nobody wants to listen for very long.</p>
<p>I tested ElevenLabs&rsquo; three voice approaches and only one worked. Speech-to-Speech converts a voice but doesn&rsquo;t translate. The Dubbing API does translate but creates the voice on its own, with no way to force a specific clone. Only Text-to-Speech could solve this: take the English <code>.srt</code> I already had, send each block to the TTS endpoint with my cloned voice, and assemble the audio aligned to the original video.</p>
<p>But having a translated <code>.srt</code> is not enough. Raw subtitle translation doesn&rsquo;t survive TTS. Passages with source code, URLs, hex hashes, lists of shell commands — the model switches to spelling mode and the audio comes out at twice the expected duration. Translations that run too long blow past the time window and need to be condensed so the voice doesn&rsquo;t sound like an auctioneer. Truncated SRTs need to be completed. And after each fix, you re-run the pipeline, listen, find the next problem, fix it, re-run. The whole thing was a cycle of interruptions, manual corrections, and re-runs — a far cry from the &ldquo;press a button and get a dub&rdquo; fantasy.</p>
<p>The big challenge was the accent. My cloned voice was trained on Brazilian Portuguese audio, so when it tries to speak English, the thick accent comes through. ElevenLabs has an <code>[American accent]</code> tag that works on v3, but on top of a voice trained in another language it&rsquo;s weak — the Brazilian accent still bleeds through. The fix was to train a second voice of mine, English only. I recorded a few minutes in my best American accent, uploaded it as a separate Instant Voice Clone in my account, named it &ldquo;Akita English&rdquo;, and set the pipeline to use that voice by default. The result comes out more natural, no tag needed, and the voice identity is still mine.</p>
<h3>The honest ceiling on my English voice quality<span class="hx:absolute hx:-mt-20" id="the-honest-ceiling-on-my-english-voice-quality"></span>
    <a href="#the-honest-ceiling-on-my-english-voice-quality" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Let&rsquo;s be frank. I&rsquo;m a Brazilian who speaks English, not a native speaker, and I don&rsquo;t have the time or patience to drill pronunciation for hours. A clone can only ever be as good as the source sample — if the source is a non-native reading a script, the clone inherits every imperfection. No clone is going to make me sound like &ldquo;Fabio speaking perfect English&rdquo; because that sound doesn&rsquo;t exist for the model to learn from.</p>
<p>To get to the version that&rsquo;s running today, I recorded five different training samples throughout the day, each with a different rhythm and script, and ran an A/B battery against the original Portuguese. None sounded native, and that was never the goal. The realistic bar was more modest: sound recognizably like me, read technical content without stumbling, and carry a full one-hour episode without wearing the listener out. The winner ended up being the first iteration, the original &ldquo;Akita English&rdquo;. Far from perfect, but good enough to ship. Swapping it out later is literally one line in the config file.</p>
<p>What surprised me most along the way was realizing that the rhythm of the training sample matters more than the timbre. The clone learns the cadence of whoever recorded it: iteration 5 came out 25% slower than iteration 2 reading the exact same text, purely because I recorded that particular sample more slowly. For dubbing tech videos, the right target is the rhythm of a &ldquo;YouTube tech explainer&rdquo; — brisk, energetic, around 150 words per minute. No audiobook pacing.</p>
<h3>How I align audio to the original video<span class="hx:absolute hx:-mt-20" id="how-i-align-audio-to-the-original-video"></span>
    <a href="#how-i-align-audio-to-the-original-video" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>A non-obvious point: spoken English tends to be longer than equivalent Portuguese. If you just generate the audio for each subtitle and paste it at the original timestamp, the dub drifts later and later. With 30 minutes of video you&rsquo;re already several seconds out of sync.</p>
<p>My approach was to attack the problem in layers. The first version split the SRT into smaller blocks, called &ldquo;chunks&rdquo;. Each chunk maxed out at 700 characters (so v3 wouldn&rsquo;t hallucinate) and could only cut at a sentence boundary, never mid-sentence. A simple algorithm accumulated subtitles into a buffer until it was about to overflow, then walked back looking for the last cue whose text ended with <code>.</code>, <code>!</code>, or <code>?</code>, and flushed only up to there. The cues still in the buffer got rolled into the next chunk. Each chunk also stored the start and end timestamps of the subtitles it covered, so the assembly step knew exactly where to drop that piece of audio later.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># For every incoming cue, check whether adding it</span>
</span></span><span class="line"><span class="cl"><span class="c1"># would push the buffer past the char cap.</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">cue</span> <span class="ow">in</span> <span class="n">cues</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">projected</span> <span class="o">=</span> <span class="n">buffer_char_len</span><span class="p">(</span><span class="n">buf</span><span class="p">)</span> <span class="o">+</span> <span class="nb">len</span><span class="p">(</span><span class="n">cue</span><span class="o">.</span><span class="n">text</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">projected</span> <span class="o">&gt;</span> <span class="n">max_chars</span> <span class="ow">and</span> <span class="n">buf</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Walk back for the last cue in the buffer that</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># ends in &#39;.&#39;, &#39;!&#39;, &#39;?&#39;, or &#39;…&#39;.</span>
</span></span><span class="line"><span class="cl">        <span class="n">sentence_end</span> <span class="o">=</span> <span class="n">last_sentence_end_index</span><span class="p">(</span><span class="n">buf</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">sentence_end</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="n">buf</span> <span class="o">=</span> <span class="n">flush</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span> <span class="n">sentence_end</span><span class="p">,</span> <span class="n">chunks</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="c1"># Run-on sentence longer than max_chars with no</span>
</span></span><span class="line"><span class="cl">            <span class="c1"># terminator. Rare, but happens. Warn and cut.</span>
</span></span><span class="line"><span class="cl">            <span class="n">log</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="s2">&#34;run-on sentence &gt; </span><span class="si">%d</span><span class="s2"> chars — &#34;</span>
</span></span><span class="line"><span class="cl">                        <span class="s2">&#34;splitting mid-sentence as a last resort&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                        <span class="n">max_chars</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">buf</span> <span class="o">=</span> <span class="n">flush</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">buf</span><span class="p">),</span> <span class="n">chunks</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">buf</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">cue</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Soft-break: if the buffer is already at least 60% full</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># AND the current cue ended a sentence, flush now. Keeps</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># chunks balanced around 60-100% of max.</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">cue</span><span class="o">.</span><span class="n">ends_sentence</span> <span class="ow">and</span> <span class="n">buffer_char_len</span><span class="p">(</span><span class="n">buf</span><span class="p">)</span> <span class="o">&gt;=</span> <span class="n">max_chars</span> <span class="o">*</span> <span class="mf">0.6</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">buf</span> <span class="o">=</span> <span class="n">flush</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">buf</span><span class="p">),</span> <span class="n">chunks</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Technically, this was the turning point: instead of letting the dub follow the visual structure of the subtitle file, the chunker started following the real paragraph structure of the script. I won&rsquo;t retell the whole story of how I got there and why it eventually forced a mass rerender, because I come back to that later in the section about the problems that only showed up after the first upload. The technical point here is simple: once the window represented a real spoken paragraph, the audio started sounding natural, and the stretch target could move from 92% to 95%.</p>
<p>Once the chunking is done, there&rsquo;s still the problem that English runs longer than the Portuguese equivalent when spoken. The trick here is predicting whether a chunk is going to blow past its target window <strong>before</strong> generating the audio. If the ratio <code>characters / 16 chars-per-second</code> already exceeds the target duration by a margin, the pipeline passes the <code>speed</code> parameter directly to the ElevenLabs API, asking the model to natively generate faster speech. This preserves prosody way better than compressing afterwards. The ceiling is 1.15× (beyond that the voice starts sounding rushed).</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># before calling the API, predict if it will overrun</span>
</span></span><span class="line"><span class="cl"><span class="n">expected_sec</span> <span class="o">=</span> <span class="n">char_count</span> <span class="o">/</span> <span class="n">EXPECTED_CHARS_PER_SEC</span>  <span class="c1"># 16 chars/s</span>
</span></span><span class="line"><span class="cl"><span class="n">predicted_ratio</span> <span class="o">=</span> <span class="n">expected_sec</span> <span class="o">/</span> <span class="n">target_sec</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">voice_speed</span> <span class="o">=</span> <span class="mf">1.0</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="n">predicted_ratio</span> <span class="o">&gt;</span> <span class="n">PREEMPTIVE_SPEED_THRESHOLD</span><span class="p">:</span>   <span class="c1"># 1.05</span>
</span></span><span class="line"><span class="cl">    <span class="n">voice_speed</span> <span class="o">=</span> <span class="nb">min</span><span class="p">(</span><span class="n">predicted_ratio</span> <span class="o">*</span> <span class="mf">0.98</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                      <span class="n">PREEMPTIVE_SPEED_MAX</span><span class="p">)</span>         <span class="c1"># cap at 1.15</span>
</span></span><span class="line"><span class="cl">    <span class="n">voice_settings</span><span class="p">[</span><span class="s2">&#34;speed&#34;</span><span class="p">]</span> <span class="o">=</span> <span class="n">voice_speed</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Even with preemptive <code>speed</code> turned on, sometimes the generated audio still comes out a tiny bit longer than the target window. That&rsquo;s what the second safety net is for: measure the actual audio with <code>ffprobe</code> after it&rsquo;s generated, compare to the target window, and apply the <code>ffmpeg</code> <code>atempo</code> filter to compress in post if it exceeded the 2% tolerance, capped at 1.20×. Combining native <code>speed</code> (1.15×) with <code>atempo</code> (1.20×) gives an effective compression ceiling of 1.38×, enough to fit naturally even in the worst cases without breaking quality.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># after generating the chunk</span>
</span></span><span class="line"><span class="cl"><span class="n">actual</span> <span class="o">=</span> <span class="n">ffprobe_duration</span><span class="p">(</span><span class="n">chunk_path</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">ratio</span> <span class="o">=</span> <span class="n">actual</span> <span class="o">/</span> <span class="n">target_sec</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="n">ratio</span> <span class="o">&gt;</span> <span class="n">FIT_TOLERANCE</span><span class="p">:</span>       <span class="c1"># 1.02</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># compress the audio without touching pitch</span>
</span></span><span class="line"><span class="cl">    <span class="n">ffmpeg_atempo</span> <span class="o">=</span> <span class="nb">min</span><span class="p">(</span><span class="n">ratio</span><span class="p">,</span> <span class="n">FIT_MAX_ATEMPO</span><span class="p">)</span>   <span class="c1"># 1.20</span>
</span></span><span class="line"><span class="cl">    <span class="n">apply_atempo</span><span class="p">(</span><span class="n">chunk_path</span><span class="p">,</span> <span class="n">ffmpeg_atempo</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>When a generated chunk comes out shorter than the target window, the pipeline slightly stretches the audio (without affecting pitch) to reduce the ugly silence after it ends. The goal isn&rsquo;t to fill the whole window — a natural pause between sentences is fine — just to smooth over the worst offenders that would feel like &ldquo;cut audio&rdquo;.</p>
<p>On a big episode, 95 minutes, 194 chunks, the total cumulative drift came out to -0.7%. About 43 seconds of drift across the entire episode, imperceptible while you&rsquo;re watching.</p>
<p>What saved the budget on this architecture is that each chunk lives on disk as its own <code>.mp3</code>. The pipeline keeps a manifest with the normalized text of every chunk, and before calling the ElevenLabs API, it compares the current text against the cache. If the text hasn&rsquo;t changed, it reuses the existing audio without spending a single credit. If I rewrite a problematic cue, only the chunks affected by that cue get regenerated — the rest of the episode stays untouched.</p>
<p>This is what made the iterations viable. I&rsquo;d run the batch, listen to sections, spot a problem (translation too long, a code snippet the TTS couldn&rsquo;t pronounce, a chunk boundary that landed at a bad spot), fix the SRT or tweak the chunker parameters, and re-run. Each re-run consumed a fraction of the credits and time of the original run, because only the changed chunks were regenerated. Without this cache, every iteration would have cost nearly as much as the first pass, and the total batch cost would have been two or three times higher.</p>
<p>That chunking change also had a heavy cache and cost impact, but I&rsquo;ll leave that part for the later section where I explain the actual retracing and rerender cycle. Here, the important point is just that it was the right technical move.</p>
<h3>When <code>atempo</code> can&rsquo;t save you: rewriting cues before TTS<span class="hx:absolute hx:-mt-20" id="when-atempo-cant-save-you-rewriting-cues-before-tts"></span>
    <a href="#when-atempo-cant-save-you-rewriting-cues-before-tts" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Not everything is about window alignment. There&rsquo;s a kind of cue that breaks the whole pipeline, where neither preemptive <code>speed</code> nor post-process <code>atempo</code> can save it, and I only ran into it once I started running the batch over the more technical episodes of the channel.</p>
<p>The problem is this. The v3 reads at about 16 characters per second when you hand it normal conversational text. But when you hand it a cue stuffed with a literal URL, a hex hash, a binary string, a long sorted number list, or a shell code block, the model shifts into &ldquo;spelling it out letter by letter&rdquo; mode and drops to about 9 characters per second. A 500-character cue that was supposed to turn into 30 seconds of audio comes out at 55. The sanity check rejects it (because it&rsquo;s past 1.8×), the automatic retry tries the same 500 characters again, the five retries all fail in a row, and the chunk gets stuck.</p>
<p>And this did not happen by accident. I used to write those passages that way because I knew the same scripts would later become blog posts, so it made sense to leave the raw URL, the full command, the complete hash, the whole technical detail there for the reader. In Portuguese that worked fine because it matched my original workflow exactly: record the video, then publish the matching text on the blog. What I had never prepared for was the next step, turning that same material into automated English dubbing. I only realized that conflict when the pipeline started failing for real.</p>
<p>I hit this first on ep052, the Ubuntu beginner&rsquo;s guide, where two cues carried <code>github.com</code> URLs, <code>hkp://keyserver.ubuntu.com</code> URLs, and a 40-character GPG hash. Trying to fix that in post-processing is a waste of time. The 1.20× <code>atempo</code> ceiling will never compress 55 seconds into 30, and even if it did, the result would be an unlistenable chipmunk. The fix has to land upstream, in the text itself.</p>
<p>The solution was to rewrite the cue so it describes what the command does instead of showing the literal command:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-diff" data-lang="diff"><span class="line"><span class="cl"><span class="gd">- Run: apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 \
</span></span></span><span class="line"><span class="cl"><span class="gd">-   --recv-keys 0A6A3E7F79F93EF8AAB9E92BAEBB74C8B5A1E44D
</span></span></span><span class="line"><span class="cl"><span class="gi">+ Run the full command in the video description to import the
</span></span></span><span class="line"><span class="cl"><span class="gi">+ signing key from the Ubuntu keyserver.
</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The on-screen English caption still shows the full command, so anyone reading the caption sees exactly what they need to type. The dubbed audio describes the intent in spoken language, which is what the TTS can deliver without choking. The instruction to the viewer stays intact, the characters-per-second ratio goes back to the expected 16, and the generated audio fits the original window without needing any stretch or compression in post.</p>
<p>I went hunting for cues like this across the whole batch. I scanned from ep052 through ep146 looking for cues with a high density of &ldquo;hard&rdquo; characters (digits, brackets, operators, long URLs, binary, hex) and ended up rewriting around 30 cues across 13 episodes:</p>
<ul>
<li>ep052 Ubuntu for devs: GitHub URLs and a 40-character GPG hash</li>
<li>ep091 Hello World in C: binary examples</li>
<li>ep095 640kB memory: address wrap, segment-offset</li>
<li>ep106 CodeMiner: spelled-out email address</li>
<li>ep113 Compression: pure binary strings, 75% hard characters</li>
<li>ep115 SQL Server: long sorted number list</li>
<li>ep120 Internet: dotted IP addresses</li>
<li>ep121 Sockets: two JavaScript code blocks</li>
<li>ep122 Proxies: Chrome User-Agent headers</li>
<li>ep123 Secure networking: shell one-liners, Docker flags</li>
<li>ep126 Gentoo: C <code>chroot</code> demo</li>
<li>ep136 Containers: GitHub release URL</li>
<li>ep144 Cryptography: signing key address</li>
</ul>
<p>The pattern was always the same: keep the caption showing the code, URL, or hash verbatim, and rewrite the spoken text so the narrator describes what it does. I could have automated the rewrite by letting an LLM read each cue and propose a substitution on the fly, but it felt safer to review them all by hand. It&rsquo;s exactly the kind of thing where the model decides to &ldquo;improve&rdquo; a command to make it cleaner and ships with broken shell. The manual sweep took about two hours. The automated version would have cost the same time in review, with the bonus anxiety of having shipped a mangled command by accident.</p>
<p>There was also one weird structural case on ep146, about Docker Compose. Two cues had glued themselves together because of a missing blank line in the SRT, and <code>pysrt</code> was treating them as one giant cue. The TTS never even got to process it — the chunker choked first. I fixed it by hand, adding the missing blank line. One-character fix, half an hour to track down.</p>
<h3>Reconstructing truncated SRTs<span class="hx:absolute hx:-mt-20" id="reconstructing-truncated-srts"></span>
    <a href="#reconstructing-truncated-srts" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Another trap showed up when I went to run the full batch: three episodes had English captions that were too short relative to the actual video length. Ep056, the Rails episode, was the most absurd case. Nineteen minutes of caption for an 80-minute video. Sixty-two minutes of content with no caption at all. Ep057 (WSL 2) was missing 14 minutes, and ep068 (Git Direito) was missing 2.</p>
<p>This is a leftover from my old translation workflow. At some point I started hand-revising the caption, stopped partway through, and the <code>.en.srt</code> file got saved truncated at the spot where I stopped. You can&rsquo;t dub an 80-minute video with a 19-minute caption. The chunker produces audio up to where it can read and then simply has no idea what to do with the rest of the video.</p>
<p>The fix became a new script. It diffs the truncated <code>.en.srt</code> against the <code>.pt-orig.srt</code> (the raw YouTube auto-caption, which always covers the whole video), picks up the cues that only exist in Portuguese, sends them to Claude Sonnet 4.6 with a strict JSON schema asking for cue-by-cue translation, and pastes the result back at the tail of the English file. The schema is the detail that matters. When I tried asking for the response as plain text in <code>N|text</code> format, Claude was dropping about 20% of the cues along the way. With a strict JSON schema, the drop rate fell to zero across the three repairs.</p>
<p>The numbers:</p>
<ul>
<li>ep068 Git Direito: +50 cues / 2 minutes reconstructed</li>
<li>ep057 WSL 2: +368 cues / 14 minutes</li>
<li>ep056 Rails: +1445 cues / 62 minutes (plus dropping 5 fake cues the old translator had invented at the tail to plug the gap)</li>
</ul>
<p>After the repairs, the three episodes joined the batch normally and got dubbed like any other. The kind of tool you hope you never need, but when you need it, it&rsquo;s worth writing once and closing the problem for good.</p>
<h3>Automatic emotion tags<span class="hx:absolute hx:-mt-20" id="automatic-emotion-tags"></span>
    <a href="#automatic-emotion-tags" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>While I was testing, I decided to add one more step to the pipeline: an &ldquo;emotion tagger&rdquo; that reads the English SRT before it goes to TTS and inserts tags like <code>[sarcastic]</code>, <code>[thoughtful]</code>, <code>[emphatic]</code>, <code>[deadpan]</code> at spots where a human narrator would naturally shift tone. The idea is to mimic what a professional voice actor would do — hit the emphasis at the right moments — without turning the video into a theater of overblown emotions.</p>
<p>This part is tricky for two reasons. First: letting an LLM loose on your SRT runs a real risk of it &ldquo;improving&rdquo; the text (swapping a word here, rewriting a sentence there) and you ending up with a dub that doesn&rsquo;t match the caption. Second: LLMs love to overdo it. Ask them to tag emotion and they&rsquo;ll put a tag on every other line. To guard against both, I pinned a small allow-list of tags and run a round-trip validation after Claude&rsquo;s response comes back:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">ALLOWED_TAGS</span><span class="p">:</span> <span class="nb">frozenset</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="nb">frozenset</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;[sarcastic]&#34;</span><span class="p">,</span> <span class="s2">&#34;[thoughtful]&#34;</span><span class="p">,</span> <span class="s2">&#34;[emphatic]&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;[deadpan]&#34;</span><span class="p">,</span> <span class="s2">&#34;[serious]&#34;</span><span class="p">,</span> <span class="s2">&#34;[amused]&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;[sighs]&#34;</span><span class="p">,</span> <span class="s2">&#34;[exasperated]&#34;</span><span class="p">,</span> <span class="s2">&#34;[confident]&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;[matter-of-fact]&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">validate_tagged</span><span class="p">(</span><span class="n">original</span><span class="p">,</span> <span class="n">tagged</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;&#34;&#34;Ensure the tagged SRT preserves the original with no drift.&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">original</span><span class="p">)</span> <span class="o">!=</span> <span class="nb">len</span><span class="p">(</span><span class="n">tagged</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="k">raise</span> <span class="n">TagValidationError</span><span class="p">(</span><span class="s2">&#34;cue count mismatch&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="n">o</span><span class="p">,</span> <span class="n">t</span> <span class="ow">in</span> <span class="nb">zip</span><span class="p">(</span><span class="n">original</span><span class="p">,</span> <span class="n">tagged</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># Index and timestamp must match byte for byte.</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">o</span><span class="o">.</span><span class="n">index</span> <span class="o">!=</span> <span class="n">t</span><span class="o">.</span><span class="n">index</span> <span class="ow">or</span> <span class="n">o</span><span class="o">.</span><span class="n">timestamp</span> <span class="o">!=</span> <span class="n">t</span><span class="o">.</span><span class="n">timestamp</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">raise</span> <span class="n">TagValidationError</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;cue </span><span class="si">{</span><span class="n">o</span><span class="o">.</span><span class="n">index</span><span class="si">}</span><span class="s2">: header drift&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1"># Any tag outside the allow-list invalidates the whole response.</span>
</span></span><span class="line"><span class="cl">        <span class="n">bad</span> <span class="o">=</span> <span class="n">find_disallowed_tags</span><span class="p">(</span><span class="n">t</span><span class="o">.</span><span class="n">text</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">bad</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">raise</span> <span class="n">TagValidationError</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;cue </span><span class="si">{</span><span class="n">o</span><span class="o">.</span><span class="n">index</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">bad</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1"># The text with the tags stripped must be byte-identical to</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># the original. If the LLM swapped a word, rewrote anything,</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># validation fails and the response is rejected.</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">strip_tags</span><span class="p">(</span><span class="n">o</span><span class="o">.</span><span class="n">text</span><span class="p">)</span> <span class="o">!=</span> <span class="n">strip_tags</span><span class="p">(</span><span class="n">t</span><span class="o">.</span><span class="n">text</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="k">raise</span> <span class="n">TagValidationError</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;cue </span><span class="si">{</span><span class="n">o</span><span class="o">.</span><span class="n">index</span><span class="si">}</span><span class="s2">: text drift&#34;</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>When validation fails, the pipeline throws the response away and re-requests (or, if it keeps failing, ships the SRT untagged). On tag density, the prompt sent to Claude explicitly says: aim for <code>N/10</code> tags in a SRT with N cues, hard floor of 1 tag per 12 cues, hard ceiling of 1 tag per 7 cues, distribution balanced across the four quarters of the episode, and no two tagged cues within 4 cues of each other. That rule set was the sixth iteration — the five before it either blanketed everything in tags or loaded them all into the first quarter.</p>
<h3>Don&rsquo;t let v3 hallucinate<span class="hx:absolute hx:-mt-20" id="dont-let-v3-hallucinate"></span>
    <a href="#dont-let-v3-hallucinate" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>A critical detail that only shows up in production: ElevenLabs v3 tends to hallucinate when you send very long blocks of text. I&rsquo;ve seen blocks where the input text was around 1,500 characters and the model generated <strong>nine minutes</strong> of audio when the expected duration was a minute and a half. The model just decides to keep talking on its own, inventing content.</p>
<p>The official ElevenLabs docs recommend keeping each call under 800 characters to avoid this. I went more conservative and cut at 700, always at sentence end (never mid-sentence, because a mid-sentence split sounds horrible when concatenated). On top of that, I bumped <code>stability</code> to 0.9 (Robust mode, more stable but less responsive to emotion tags), turned on <code>apply_text_normalization</code> so the model pronounces numbers and acronyms correctly, and added a sanity check that rejects any generated audio longer than 1.8× the expected duration.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">DEFAULT_VOICE_SETTINGS</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;stability&#34;</span><span class="p">:</span> <span class="mf">0.9</span><span class="p">,</span>          <span class="c1"># Robust mode</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;similarity_boost&#34;</span><span class="p">:</span> <span class="mf">0.95</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;style&#34;</span><span class="p">:</span> <span class="mf">0.0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;use_speaker_boost&#34;</span><span class="p">:</span> <span class="kc">True</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">EXPECTED_CHARS_PER_SEC</span> <span class="o">=</span> <span class="mf">16.0</span>
</span></span><span class="line"><span class="cl"><span class="n">CHUNK_SANITY_MAX_FACTOR</span> <span class="o">=</span> <span class="mf">1.8</span>   <span class="c1"># reject if actual &gt; 1.8× expected</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>With those mitigations in place, a 95-minute episode (194 blocks) ran start to finish with zero hallucinations. Exactly one block failed mid-run on a transient 502 error from the API, which the automatic retry caught on the next attempt.</p>
<h3>Mastering for YouTube<span class="hx:absolute hx:-mt-20" id="mastering-for-youtube"></span>
    <a href="#mastering-for-youtube" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>YouTube doesn&rsquo;t publish an official number, but industry consensus is that its playback loudness normalization targets -14 LUFS. Deliver audio louder than that and YouTube turns it down; deliver it quieter and it leaves it alone. To land on the target exactly, the pipeline runs <code>ffmpeg</code>&rsquo;s <code>loudnorm</code> filter in two passes. The first pass measures the entire audio and prints the stats (<code>input_i</code>, <code>input_tp</code>, <code>input_lra</code>, <code>input_thresh</code>) as a JSON blob on stderr. The second pass reads those stats back in and applies a linear static gain to land precisely at -14 LUFS, -1.5 dBTP:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Pass 1: measure</span>
</span></span><span class="line"><span class="cl">ffmpeg -i final_en.mp3 <span class="se">\
</span></span></span><span class="line"><span class="cl">  -af <span class="s2">&#34;highpass=f=80,loudnorm=I=-14:TP=-1.5:LRA=9:print_format=json&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -f null -
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Pass 2: apply linear gain with the measured values</span>
</span></span><span class="line"><span class="cl">ffmpeg -i final_en.mp3 <span class="se">\
</span></span></span><span class="line"><span class="cl">  -af <span class="s2">&#34;highpass=f=80,loudnorm=I=-14:TP=-1.5:LRA=9\
</span></span></span><span class="line"><span class="cl"><span class="s2">:measured_I=-18.3:measured_TP=-3.1:measured_LRA=5.4\
</span></span></span><span class="line"><span class="cl"><span class="s2">:measured_thresh=-28.7:offset=1.2:linear=true&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -ar <span class="m">48000</span> -ac <span class="m">2</span> -c:a pcm_s16le final_en_mastered.wav</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Two passes instead of one because single-pass mode runs in dynamic compression and ends up &ldquo;pumping&rdquo; the gain during silent stretches. With <code>linear=true</code> on pass 2, the gain stays static on top of the pass-1 measurements, so no pumping. The result lands within ±0.1 LU of the target, which is effectively inaudible. The <code>highpass=f=80</code> filter in front kills HVAC rumble and mains hum below 80 Hz, which the human ear doesn&rsquo;t catch but will move peak measurements around.</p>
<h3>The point where I thought it was done. And it wasn&rsquo;t.<span class="hx:absolute hx:-mt-20" id="the-point-where-i-thought-it-was-done-and-it-wasnt"></span>
    <a href="#the-point-where-i-thought-it-was-done-and-it-wasnt" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>This is where the most annoying, and most educational, part of the project starts.</p>
<p>I really thought I had nailed it on the first big pass. The pipeline closed all 146 episodes, I uploaded everything, and it looked done. The durations matched, the files were all there, and the whole process had finally crossed the complete batch of nearly 96 hours of video. But it was exactly after that upload that I started noticing problems I simply had not anticipated.</p>
<p>The first scare was the jingle situation. I already knew many Akitando episodes begin with that short instrumental intro before I start talking, but I underestimated how much trouble it would cause. YouTube&rsquo;s <code>.srt</code> is almost useless here, because a jingle is not speech. Sometimes it marks <code>[Music]</code>, sometimes it gives you nothing useful, sometimes the window is just wrong. In the first assembly pass, some episodes lost the jingle entirely, some got it doubled, and in others the source-fill from the original audio already contained the right jingle and my splice came in on top of it, pushing everything else forward. This was the kind of bug you only catch when you sit down and actually listen to the final mastered file, not when you just look at total duration.</p>
<p>The fix was to stop hoping the subtitle would tell me where the jingle was and go straight to the source. I built a detector using the original video audio itself plus reference jingles, with an audit and repair pass on top of the mastered outputs already generated. That eventually became a small subsystem of its own: detector, audit, cached reassembly, exact-window repair. At the end of that detour, the set of changed episodes collapsed into a well-defined reupload bucket and the jingle problem finally came under control. It was one of those classic production moments: the first solution looked good enough until it hit the whole archive.</p>
<p>Then came the second punch, and this one was more expensive: I had forgotten a basic fact about YouTube subtitles. SRT is built to be read on screen, not to become spoken script. So an idea that was a full paragraph in my original text showed up split into several small cues, each with its own micro time window. In the first version of the pipeline I was using those cues as the generation unit for ElevenLabs. The result was TTS audio that was coherent locally, but the final assembly inserted artificial silences in the middle of sentences. Technically aligned, humanly weird.</p>
<p>Fixing that hurt because it meant walking several steps back. Instead of respecting the cue structure of the SRT, I had to go back to my original scripts and rebuild chunking around the paragraphs I had actually read when recording the videos. Almost every Akitando episode has a matching blog post with a <code>## Script</code> section, and that is what saved the project. Because I had been meticulous back when I made those videos — writing the scripts first, reading exactly that on camera, and archiving both the videos and the scripts — I had a source of truth. That made it possible to map the script paragraphs onto the timings in the <code>.pt-BR.srt</code>, regroup the <code>.en.srt</code> on top of that, and regenerate the chunks in the right structure.</p>
<p>The problem is that this destroys the previous cache. Even when the text barely changes, if the chunk boundary changes then the TTS call changes too: different context, different prosody, different beginning, different ending. So this was not a touch-up. It was a full regeneration of everything again. That was the point where the cost started escaping my original estimate.</p>
<p>And just when I thought now it really was over, the third hit landed: external clips. Many videos on the channel include inserted footage from other sources. The Ruby on Rails episode, for example, has 37signals clips, Apple material, commercials, external inserts in the middle of the narrative. YouTube&rsquo;s <code>.srt</code> did not know what to do with that. In some places it simply hallucinated text, in others the timings were completely wrong, and in others the original speech had nothing to do with the subtitle I was using as ground truth. Again: if you only look at the automation, it looks fine; if you actually listen to the final master, it isn&rsquo;t.</p>
<p>The fix, again, was to leave the subtitle behind and go back to the original material. I built an external-clip window detector based on alignment gaps between transcript, <code>.pt-BR.srt</code>, speechless cues and chunks that crossed those windows. Then, instead of trying to dub what made no sense to dub, the process started recovering the original audio from the source video exactly in those stretches and filling the master with the correct source audio there. That&rsquo;s what fixed the 37signals, Apple Education, Mac vs PC commercial and other inserted segments I had forgotten were even there.</p>
<p>That was the biggest lesson from the second half of this project: automation takes you very far, but the full archive always has more dark corners than you imagine on the first pass.</p>
<h2>The dubbing batch numbers (and why it hurt more than I expected)<span class="hx:absolute hx:-mt-20" id="the-dubbing-batch-numbers-and-why-it-hurt-more-than-i-expected"></span>
    <a href="#the-dubbing-batch-numbers-and-why-it-hurt-more-than-i-expected" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I need to correct myself again. I didn&rsquo;t just miscalculate the first bill. I also overestimated how confident I should have been after the first full run.</p>
<p>When I upgraded to ElevenLabs Business, I thought I would finally have plenty of headroom. In practice, I didn&rsquo;t. The latest dashboard screenshot shows <strong>10,046,279 credits consumed out of a limit of 11,000,000</strong>, leaving <strong>953,721</strong>:</p>
<p><img src="/2026/04/09/como-a-elevenlabs-nao-foi-morta-pelo-qwen3-tts/elevenlabs-credits-dashboard.png" alt="ElevenLabs dashboard showing 10,046,279 credits consumed out of 11,000,000"  loading="lazy" /></p>
<p>In plain English: I burned through more than 91% of the Business workspace limit. And that helps explain what &ldquo;10 million credits&rdquo; really means in this context. It does not mean &ldquo;I rendered 146 episodes once&rdquo;. It means rendering 146 episodes, discovering a structural problem, going back, fixing it, regenerating chunks, discovering another structural problem, going back again, rebuilding masters, auditing jingles, repairing external clip windows, uploading again. Every extra round costs money. And when the change invalidates the cache, it costs a lot.</p>
<p>The cost also doesn&rsquo;t stop at ElevenLabs. All the subtitle curation, problematic cue rewrites, truncated SRT reconstruction, jingle audits, external clip detector work and the rest ran through Claude Code on the Claude Max 20× plan. Combined with the mass blog translation to English that I described in the <a href="/en/2026/04/09/20-years-of-blogging-ai-finally-translated-everything/">anniversary post</a>, the Claude Max 20× weekly limit hit 100%, with more than BRL 300 in extra usage on top. If you think generative AI becomes free once you&rsquo;ve signed up for the plan, the invoices disagree.</p>
<p>Even so, I still think it&rsquo;s worth it. We&rsquo;re talking about 146 episodes, almost 96 hours of technical content, more than 5 years of channel archive. Doing that manually in a studio, with an actor, direction, pickups and human review on everything, would cost a completely different order of magnitude.</p>
<p>What changed was how I think about the project. At the start I was looking at this as &ldquo;one very large batch&rdquo;. In the end it was something else. It was digging through a whole archive, finding hidden exceptions, fixing old rot, rebuilding masters, rerendering, relistening.</p>
<p>The first full batch took a little under two days and made me think I had solved everything. I hadn&rsquo;t. The second pass, with paragraph-aware chunking, was already expensive. The third, with jingle repairs and mastered-output fixes, cost more again. And the fourth hit came from the external clips I had not modeled correctly in the first automation pass.</p>
<p>But now, finally, I think it&rsquo;s done. Or rather: I hope it&rsquo;s done. Everything has been uploaded again, for the last time. It isn&rsquo;t perfect in the absolute sense of the word. It&rsquo;s as good as I can get it with scripts, audits and automation, without descending into manual-intervention hell episode by episode.</p>
<p>And if all of this was possible, it&rsquo;s because years ago I had the discipline to produce the videos the right way: write first, read the script when recording, and archive the materials afterward. Without the original videos and without the original scripts, I would have had no way to discover where the <code>.srt</code> was lying, where the timing was wrong, where the external clip started, where the original paragraph began and ended. That old organization is what made it possible to repair the project now.</p>
<p>So yes: the dub for all 146 episodes is done. Not perfect. But closed. At least, I sincerely hope this time it really is.</p>
<h2>The first test, watch it<span class="hx:absolute hx:-mt-20" id="the-first-test-watch-it"></span>
    <a href="#the-first-test-watch-it" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I made a test video to prove the thing works. Two caveats up front: first, the embed below already loads with English captions on by default, so that side is solvable via URL. Second, the audio is annoying: YouTube removed the audio track selector from embedded players back around March of this year. So inside the embed you won&rsquo;t find that option in the gear menu at all, it just plays in Portuguese. To actually hear the English dub, click <strong><a href="https://www.youtube.com/watch?v=QNLd8TZ_JQc"target="_blank" rel="noopener">watch directly on YouTube</a></strong> — the main YouTube watch page still has the audio track switcher in the gear menu.</p>
<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/QNLd8TZ_JQc?cc_load_policy=1&amp;cc_lang_pref=en&amp;hl=en"
    title="Akitando dubbed in English (test)"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>
<p>If you&rsquo;re used to YouTube&rsquo;s automatic dubbing or to the AI dubbing on TikTok, compare them. The difference is glaring. The voice is mine, with a worked-over American accent, and the sync with the original video stays within 1 to 2% cumulative drift, practically imperceptible over 95 minutes of video.</p>
<h2>And the missing piece: translating the thumbnails<span class="hx:absolute hx:-mt-20" id="and-the-missing-piece-translating-the-thumbnails"></span>
    <a href="#and-the-missing-piece-translating-the-thumbnails" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>While I was closing this post, the penny dropped: I&rsquo;d left a loose end. All 146 thumbnails on my channel are in Portuguese, many of them with big all-caps titles like &ldquo;9 DICAS PARA PALESTRANTES&rdquo; or &ldquo;7 RECOMENDAÇÕES DE SHOWS PARA PESSOAS DE TECH&rdquo;. Perfect English audio doesn&rsquo;t help if the image in the search results is an illegible block of text to anyone who doesn&rsquo;t read Portuguese. YouTube lets you upload an alternative thumbnail per language, so I needed a way to generate English versions, keeping the rest of the art identical and swapping only the text.</p>
<p>The tool uses two pieces. <strong><code>yt-dlp</code></strong> is the modern fork of the old <code>youtube-dl</code>, a Python CLI that downloads anything from YouTube (videos, audio, subtitles, thumbnails) without needing an API key. <strong>Nano Banana Pro</strong> (<code>nano-banana-pro-preview</code> on the API) is Google Gemini&rsquo;s latest image-editing model — you feed it an input image with a prompt and it returns an edited image, preserving the rest of the composition when you ask it to touch only a specific part.</p>
<p>The pipeline has two steps. Step 1: <code>yt-dlp</code> grabs the thumbnail as a <code>.jpg</code>, no video download:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">yt-dlp --skip-download <span class="se">\
</span></span></span><span class="line"><span class="cl">       --write-thumbnail <span class="se">\
</span></span></span><span class="line"><span class="cl">       --convert-thumbnails jpg <span class="se">\
</span></span></span><span class="line"><span class="cl">       -o <span class="s2">&#34;thumbnails/originals/&lt;slug&gt;/&lt;video_id&gt;&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">       <span class="s2">&#34;https://www.youtube.com/watch?v=&lt;video_id&gt;&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Step 2: send each image to Gemini Nano Banana Pro with a prompt that&rsquo;s a rigid contract of hard requirements, not just a &ldquo;translate this&rdquo;:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">TASK:
</span></span><span class="line"><span class="cl">Detect every piece of Portuguese text visible on this image and
</span></span><span class="line"><span class="cl">translate it to clear, natural American English. Replace the
</span></span><span class="line"><span class="cl">Portuguese text with the English translation in the same visual
</span></span><span class="line"><span class="cl">position.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">STRICT REQUIREMENTS — follow every one:
</span></span><span class="line"><span class="cl">- Preserve EVERY non-text element identically: face, pose,
</span></span><span class="line"><span class="cl">  expression, background, color palette, lighting, icons, logos,
</span></span><span class="line"><span class="cl">  decorative shapes, borders, layout. Only the text changes.
</span></span><span class="line"><span class="cl">- The English translation must be IDIOMATIC and CONFIDENT — not
</span></span><span class="line"><span class="cl">  a literal word-for-word rewrite. It&#39;s a YouTube thumbnail for a
</span></span><span class="line"><span class="cl">  tech audience, so use punchy phrasing a native English-speaking
</span></span><span class="line"><span class="cl">  tech YouTuber would write.
</span></span><span class="line"><span class="cl">- Match the original text&#39;s font family, weight, size, color,
</span></span><span class="line"><span class="cl">  stroke outline, drop shadow, and any decorative treatment.
</span></span><span class="line"><span class="cl">- If there is no Portuguese text at all on the image, return the
</span></span><span class="line"><span class="cl">  original image unchanged.</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The &ldquo;return the original image unchanged&rdquo; clause is what saves the earliest episodes, which don&rsquo;t have overlay text, just my face. Without it, the model would invariably try to &ldquo;improve&rdquo; the composition, swap the lighting, and so on.</p>
<p>Here&rsquo;s the result on two examples (episodes 10 and 11):</p>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin: 16px 0;">
<div><strong>Original (PT)</strong><br><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260409_thumb_ep010_pt.jpg" alt="Original Portuguese thumbnail: 7 Recomendações de Shows para pessoas de Tech" style="width: 100%;"></div>
<div><strong>Translated (EN)</strong><br><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260409_thumb_ep010_en.jpg" alt="Translated English thumbnail: 7 TV Shows You Must Watch If You're In Tech" style="width: 100%;"></div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin: 16px 0;">
<div><strong>Original (PT)</strong><br><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260409_thumb_ep011_pt.jpg" alt="Original Portuguese thumbnail: 9 Dicas para Palestrantes: venda sua caneta" style="width: 100%;"></div>
<div><strong>Translated (EN)</strong><br><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260409_thumb_ep011_en.jpg" alt="Translated English thumbnail: 9 Tips for Speakers: sell your pen" style="width: 100%;"></div>
</div>
<p>Notice the detail on episode 10. The model didn&rsquo;t translate &ldquo;7 Recomendações de Shows para Pessoas de Tech&rdquo; literally into &ldquo;7 Show Recommendations for Tech People&rdquo; the way Google Translate would. It rewrote it as &ldquo;7 TV Shows You Must Watch If You&rsquo;re In Tech&rdquo;, the way an actual American tech YouTuber would headline the video. The rest of the image stays pixel-for-pixel identical. On episode 11, &ldquo;9 DICAS PARA PALESTRANTES: venda sua caneta&rdquo; becomes &ldquo;9 TIPS FOR SPEAKERS: sell your pen&rdquo;, with the font, all-caps treatment and position preserved. If you didn&rsquo;t know the original was in Portuguese, you couldn&rsquo;t guess the second image is an automatic AI edit.</p>
<p>Cost on the Gemini side: a few cents per image, a handful of dollars total for the whole batch. Trivial next to the $1,500+ on the audio dub. With the thumbnail sorted, the channel&rsquo;s English conversion is finally buttoned up — ElevenLabs v3 cloning the voice over hand-curated English <code>.srt</code> files, and Nano Banana Pro editing the image so the title matches the audio.</p>
<h2>The conclusion, which is the title of this post<span class="hx:absolute hx:-mt-20" id="the-conclusion-which-is-the-title-of-this-post"></span>
    <a href="#the-conclusion-which-is-the-title-of-this-post" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>When Qwen3 TTS dropped, the hype was calling it an &ldquo;ElevenLabs killer&rdquo;. I spent weeks trying to live with that premise in practice, with real content shipping every week. And what I found is that open source TTS is still miles behind ElevenLabs. The gap is large, and it&rsquo;s a gap that shows up precisely when you step off the 5-second demo tweet and put the model to work on a real 30-minute weekly podcast.</p>
<p>In practice, Qwen3 doesn&rsquo;t even beat ElevenLabs&rsquo; v2 model. v3, which is the current one, is still a step above v2. The prosody comes out better, the inline emotion tags work in Portuguese and English without effort, and the API stays up without you maintaining a server. The cost per character is a bit higher, but for my current volume it sits comfortably within budget and gives me back the hours I was spending on GPU babysitting.</p>
<p>The lesson here is the same one the LLM crowd is learning slowly. A 30-second demo tweet is one thing, production is a completely different thing. Open source has its niche, mainly when you have sensitive data that can&rsquo;t leave the house, or when you have tons of idle GPU and little recurring budget. But for serious TTS use in a commercial product, here in April 2026, ElevenLabs is still untouchable. Qwen3 didn&rsquo;t kill anyone.</p>
<p>And don&rsquo;t forget: subscribe to <a href="https://open.spotify.com/show/7MzG2UB7IAkC3GAwEXEIVD"target="_blank" rel="noopener">The M.Akita Chronicles on Spotify</a> so you don&rsquo;t miss new episodes like this one, made with the new pipeline.</p>
]]></content:encoded><category>generative-media</category><category>local-models</category><category>reviews</category></item><item><title>Testing Open Source and Commercial LLMs - Can Anyone Beat Claude Opus?</title><link>https://akitaonrails.github.io/en/2026/04/05/testing-llms-open-source-and-commercial-can-anyone-beat-claude-opus/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/04/05/testing-llms-open-source-and-commercial-can-anyone-beat-claude-opus/</guid><pubDate>Sun, 05 Apr 2026 18:00:00 -0300</pubDate><description>&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Obsolete article (updated 2026-04-24).&lt;/strong&gt; The conclusions and rankings in this post were superseded after I re-audited the benchmark against the &lt;code&gt;ruby_llm&lt;/code&gt; gem source and restructured the evaluation criteria. Several &amp;ldquo;hallucinations&amp;rdquo; I had cataloged were actually valid API. Kimi K2.6 and Gemini 3.1 Pro moved up to Tier A. GLM 5.1 dropped to Tier C. MiMo V2.5 Pro fell from &amp;ldquo;first non-Anthropic Tier 1&amp;rdquo; to Tier B. &lt;strong&gt;The canonical version lives at &lt;a href="https://akitaonrails.github.io/en/2026/04/24/llm-benchmarks-parte-3-deepseek-kimi-mimo/"&gt;LLM Coding Benchmark (April 2026)&lt;/a&gt;.&lt;/strong&gt; This post stays as a historical record of what I concluded before the re-audit.&lt;/p&gt;</description><content:encoded><![CDATA[<blockquote>
  <p>⚠️ <strong>Obsolete article (updated 2026-04-24).</strong> The conclusions and rankings in this post were superseded after I re-audited the benchmark against the <code>ruby_llm</code> gem source and restructured the evaluation criteria. Several &ldquo;hallucinations&rdquo; I had cataloged were actually valid API. Kimi K2.6 and Gemini 3.1 Pro moved up to Tier A. GLM 5.1 dropped to Tier C. MiMo V2.5 Pro fell from &ldquo;first non-Anthropic Tier 1&rdquo; to Tier B. <strong>The canonical version lives at <a href="/en/2026/04/24/llm-benchmarks-parte-3-deepseek-kimi-mimo/">LLM Coding Benchmark (April 2026)</a>.</strong> This post stays as a historical record of what I concluded before the re-audit.</p>

</blockquote>
<hr>
<p><strong>Update April 16, 2026:</strong> Added Claude Opus 4.7, Qwen 3.6 and GPT 5.4 via Codex CLI (xHigh reasoning). Opus 4.7 is an incremental improvement over 4.6 (28 tests vs 16, same correct API) and becomes the new baseline. GPT 5.4, previously Tier 1 based on my personal vouch, now has objective data and dropped to Tier 2 — burned 7.6M tokens (~$16/run, 15x more expensive than Opus) and got the <code>add_message</code> calling convention wrong on multi-turn. Qwen 3.6 Plus remains Tier 3 with the same API hallucination as 3.5. The conclusion stands: if you want safety, pick Opus.</p>
<hr>
<p><strong>TL;DR:</strong> If you don&rsquo;t want to read the whole analysis: the only models that produced code that actually works in our benchmark were Claude Opus 4.7, Claude Opus 4.6, GLM 5 and GLM 5.1 (from Z.AI, ~89% cheaper than Opus). Sonnet works in this benchmark too, but in practice it falls short on projects requiring deeper reasoning — details in the conclusion. GPT 5.4, previously Tier 1 based on my personal vouch, now has objective data via Codex CLI: it burned 7.6M tokens ($16/run) and got the <code>add_message</code> calling convention wrong — works on the first message, breaks on multi-turn. Dropped to Tier 2. Everything else — Kimi, DeepSeek, MiniMax, Qwen, Gemini, Grok 4.20 — invented APIs that don&rsquo;t exist or ignored the gem we asked for.</p>
<p>There&rsquo;s a new wrinkle in this update: I redid the local part of the benchmark on an RTX 5090 (instead of the AMD Strix Halo) and added a fresh batch of Qwen models, including a Qwen 3.5 27B distilled directly from Claude 4.6 Opus. That reopened the conversation on running open source models locally. The 5090&rsquo;s memory bandwidth flips the game from &ldquo;unworkable&rdquo; to &ldquo;workable with 1-2 follow-up prompts.&rdquo; The bottleneck for open source models has moved to a lack of factual knowledge about specific libraries, which I unpack in detail in the new section on the Qwen family. The Claude distillation gamble, by the way, gave a pretty frustrating result that I haven&rsquo;t seen documented in these terms before.</p>
<hr>
<p>If you&rsquo;ve been following <a href="/en/en/tags/vibecoding/">my previous vibe coding pieces</a>, you know I spent the last two months in a 500-hour marathon using Claude Opus as my main coding agent. The results were good, as I reported in the <a href="/en/2026/03/05/37-days-of-vibe-coding-immersion-conclusions-on-business-models/">conclusion about business models</a>. But there was an itch I couldn&rsquo;t scratch: am I locked into one model? Is there a real alternative to Claude Opus for daily use on real projects?</p>
<p>I&rsquo;ve got an RTX 5090 with 32 GB of GDDR7. I know I can run the latest open source models. I bought a <a href="/en/2026/03/31/minisforum-ms-s1-max-amd-ai-max-395-review/">Minisforum MS-S1</a> with an AMD Ryzen AI Max 395 and 128 GB of unified memory, and built a <a href="/en/2026/03/31/migrating-my-home-server-with-claude-code/">home server with Docker</a> to serve local models. The infrastructure was ready. What was missing was actually testing it.</p>
<p>I built an automated benchmark to compare open source and commercial models under identical conditions. 33 models configured in total (25 from the original run plus 8 added in the NVIDIA rerun), 27 executed, 16 completed in some form. The code is on <a href="https://github.com/akitaonrails/llm-coding-benchmark"target="_blank" rel="noopener">GitHub</a>.</p>
<h2>The bottleneck nobody explains: VRAM and KV Cache<span class="hx:absolute hx:-mt-20" id="the-bottleneck-nobody-explains-vram-and-kv-cache"></span>
    <a href="#the-bottleneck-nobody-explains-vram-and-kv-cache" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Before getting to the results, I have to explain why running large models locally is much harder than it looks.</p>
<p>Take Qwen3 32B. The model in FP16 (full precision) takes ~64 GB. Quantized to Q4 (4 bits), it drops to ~19 GB. So it fits in my RTX 5090&rsquo;s 32 GB, right? Wrong. That&rsquo;s just the model weights. There&rsquo;s a part nobody tells you about: the <strong>KV Cache</strong>.</p>
<p>KV Cache is the memory the model uses to &ldquo;remember&rdquo; what it has already read. Every time it processes a token (a word or piece of a word), it computes two vectors — K (key) and V (value) — for every attention layer. Those vectors stick around so the model doesn&rsquo;t have to recompute everything when it generates the next token. Without that, generation would be quadratically slow.</p>
<p>The KV Cache scales linearly with the size of the context. The formula:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>KV Memory = 2 × Layers × KV_Heads × Head_Dimension × Bytes_per_Element × Context_Tokens</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>For a model like Llama 3.1 70B in BF16, that comes out to ~0.31 MB per token. Sounds tiny, until you realize that a 128K context eats <strong>40 GB</strong> of KV Cache alone. The model itself plus KV Cache adds up to way more VRAM than most GPUs have.</p>
<p>And for actual coding agent use, 128K tokens isn&rsquo;t a luxury, it&rsquo;s the bare minimum. The agent has to read files, keep conversation history, receive command output. In long benchmark sessions, our models consumed between 39K and 156K tokens. Less than 100K of context isn&rsquo;t practical for day-to-day project work.</p>
<p>Google published <a href="https://research.google/blog/turboquant-redefining-ai-efficiency-with-extreme-compression/"target="_blank" rel="noopener">TurboQuant</a> (ICLR 2026), which compresses the KV Cache to 3 bits without accuracy loss — a 6x memory reduction and up to 8x speedup. It uses random vector rotation (PolarQuant) followed by a 1-bit algorithm on the residuals. Works online during inference, compressing on write and decompressing on read. Not yet implemented in the runtimes we use (llama.cpp, Ollama), but when it lands it&rsquo;ll change the equation a lot.</p>
<p>For anyone wanting to dig deeper into the VRAM math, I recommend <a href="https://x.com/TheAhmadOsman/status/2040103488714068245"target="_blank" rel="noopener">this link from Ahmad Osman</a> for the article &ldquo;GPU Memory Math for LLMs (2026 Edition)&rdquo;.</p>
<h2>The hardware problem: not all memory is created equal<span class="hx:absolute hx:-mt-20" id="the-hardware-problem-not-all-memory-is-created-equal"></span>
    <a href="#the-hardware-problem-not-all-memory-is-created-equal" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>&ldquo;But I have 128 GB of RAM!&rdquo; Cool, but that&rsquo;s not what matters. What matters is memory bandwidth, and the difference between types is wild:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/05/llm-benchmark/en/memory-bandwidth.png" alt="Memory bandwidth by type"  loading="lazy" /></p>
<p>The RTX 5090 has 7x the bandwidth of the LPDDR5x memory in my Minisforum. That means even if a model fits in the AMD&rsquo;s unified RAM, inference will be proportionally slower. On my Minisforum with LPDDR5x at 256 GB/s, Qwen3 32B runs at ~7 tok/s. On the RTX 5090 at 1,792 GB/s, it&rsquo;d be much faster — if it fit entirely in VRAM alongside the KV Cache.</p>
<p>Most folks running local models are still on DDR4. At 50 GB/s, 32B models are basically unusable. And there&rsquo;s another factor people forget: storage. When the RAM can&rsquo;t keep up and the system swaps, the storage speed becomes the bottleneck:</p>
<table>
  <thead>
      <tr>
          <th>Storage</th>
          <th style="text-align: right">Sequential Speed</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SATA SSD</td>
          <td style="text-align: right">~550 MB/s</td>
      </tr>
      <tr>
          <td>NVMe Gen3</td>
          <td style="text-align: right">~3,500 MB/s</td>
      </tr>
      <tr>
          <td>NVMe Gen4</td>
          <td style="text-align: right">~7,000 MB/s</td>
      </tr>
      <tr>
          <td>NVMe Gen5</td>
          <td style="text-align: right">~12,000 MB/s</td>
      </tr>
  </tbody>
</table>
<p>From SATA to NVMe Gen5 you&rsquo;re looking at a 22x difference. If you&rsquo;re doing partial offloading to disk (which is common when the model doesn&rsquo;t fit entirely on the GPU), NVMe Gen4 or Gen5 makes a real difference. SATA is a non-starter.</p>
<p>To sum up: running local models isn&rsquo;t just &ldquo;having enough RAM.&rdquo; You need the right kind of memory, with the right bandwidth, and fast storage as a fallback. For a lot of people, a Mac Studio with high-bandwidth unified memory (up to 800 GB/s on the M4 Ultra with 512 GB) would be the more practical option, but it costs more than US$ 10,000. The AMD Ryzen AI Max is the cheaper alternative with unified memory, but its LPDDR5 caps out at 256 GB/s.</p>
<h2>Ollama vs llama.cpp: why Ollama falls apart on benchmarks<span class="hx:absolute hx:-mt-20" id="ollama-vs-llamacpp-why-ollama-falls-apart-on-benchmarks"></span>
    <a href="#ollama-vs-llamacpp-why-ollama-falls-apart-on-benchmarks" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><a href="https://ollama.com/"target="_blank" rel="noopener">Ollama</a> is the most popular way to run local models. Install, pull the model, run. For casual use it works. But when I tried to use it for automated benchmarks with long unattended sessions, it broke in 6 different ways across 8 models:</p>
<ol>
<li>Unloads the model mid-session. On long runs, Ollama decides the model isn&rsquo;t being used and unloads it from the GPU. The agent sits there waiting for a response from a model that no longer exists.</li>
<li>Ignores the requested context. You ask for <code>num_ctx=131072</code>, Ollama accepts, then halfway through the run it reverts to the default without warning.</li>
<li>Unstable lifecycle. Asking for <code>keep_alive: 0</code> to unload doesn&rsquo;t always work. The model stays resident and blocks the next one.</li>
<li>Incompatible formats. Native bf16 variants on Ollama failed, while the same model as a Q8 GGUF from HuggingFace worked fine.</li>
</ol>
<p>The fix: migrate to <a href="https://github.com/mostlygeek/llama-swap"target="_blank" rel="noopener">llama-swap</a>, a Go wrapper that manages llama.cpp processes with hot-swap. A request comes in for a different model than the one currently loaded, it kills the current process and starts the new one. No context negotiation, no flaky lifecycle.</p>
<p>llama-swap fixed the loading of 6 of the 8 models that had failed under Ollama:</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th>Ollama</th>
          <th>llama-swap</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Gemma 4 27B</td>
          <td>HTTP 500</td>
          <td>47.6 tok/s</td>
      </tr>
      <tr>
          <td>GLM 4.7 Flash</td>
          <td>No output</td>
          <td>47.4 tok/s</td>
      </tr>
      <tr>
          <td>Llama 4 Scout</td>
          <td>Unloaded</td>
          <td>17.5 tok/s</td>
      </tr>
      <tr>
          <td>Qwen 3.5 35B</td>
          <td>Output off-spec</td>
          <td>49.7 tok/s</td>
      </tr>
      <tr>
          <td>Qwen 3.5 122B</td>
          <td>Context drift</td>
          <td>23.1 tok/s</td>
      </tr>
      <tr>
          <td>GPT OSS 20B</td>
          <td>Model not found</td>
          <td>78.3 tok/s</td>
      </tr>
  </tbody>
</table>
<p>But llama-swap isn&rsquo;t magic.</p>
<h2>Why &ldquo;just use llama.cpp&rdquo; doesn&rsquo;t fix everything<span class="hx:absolute hx:-mt-20" id="why-just-use-llamacpp-doesnt-fix-everything"></span>
    <a href="#why-just-use-llamacpp-doesnt-fix-everything" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>llama.cpp solves Ollama&rsquo;s lifecycle problems but brings its own:</p>
<p>Each model needs specific flags. GLM and Qwen 3.5 emit <code>&lt;think&gt;</code> tags that break clients if you don&rsquo;t pass <code>--reasoning-format none</code>. Gemma 4 needs build b8665+ for the tool call parser to work.</p>
<p>Not every model supports tool calling. llama.cpp needs a dedicated parser for each model&rsquo;s tool call format. Llama 4 Scout uses a &ldquo;pythonic&rdquo; format (<code>[func(param=&quot;value&quot;)]</code>) that llama.cpp simply doesn&rsquo;t parse and emits as plain text. vLLM has a parser for it, llama.cpp doesn&rsquo;t.</p>
<p>And then there are the repetition loops. Gemma 4, even with the right parser, gets into an infinite loop after ~11 tool calls in long sessions. It&rsquo;s a <a href="https://github.com/ggml-org/llama.cpp/issues/21375"target="_blank" rel="noopener">known bug</a> that PR #21418 didn&rsquo;t fully fix.</p>
<p>Tool calling compatibility per model:</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th>Tool Calling</th>
          <th>Required Flags</th>
          <th>Benchmark Result</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Gemma 4 27B</td>
          <td>Partial (b8665+)</td>
          <td><code>--jinja --reasoning-format none</code></td>
          <td>Infinite loop after ~11 steps</td>
      </tr>
      <tr>
          <td>GLM 4.7 Flash</td>
          <td>Yes</td>
          <td><code>--jinja --reasoning-format none</code></td>
          <td>2029 files, ended mid-tool-call</td>
      </tr>
      <tr>
          <td>Qwen 3.5 (35B, 122B)</td>
          <td>Yes</td>
          <td><code>--jinja --reasoning-format none</code></td>
          <td>Completed successfully</td>
      </tr>
      <tr>
          <td>Qwen 3 Coder Next</td>
          <td>Yes</td>
          <td><code>--jinja</code></td>
          <td>Completed (best local result)</td>
      </tr>
      <tr>
          <td>GPT OSS 20B</td>
          <td>Yes</td>
          <td><code>--jinja</code></td>
          <td>Tool calls ok, but app in wrong directory</td>
      </tr>
      <tr>
          <td>Llama 4 Scout</td>
          <td>No</td>
          <td>—</td>
          <td>No parser in llama.cpp</td>
      </tr>
  </tbody>
</table>
<p>At the end of the day, llama.cpp is better than Ollama for automated runs, but &ldquo;plug and play&rdquo; it ain&rsquo;t. Each model requires specific configuration, and some just don&rsquo;t work for agentic coding yet.</p>
<h2>Reasoning: models that think vs models that wing it<span class="hx:absolute hx:-mt-20" id="reasoning-models-that-think-vs-models-that-wing-it"></span>
    <a href="#reasoning-models-that-think-vs-models-that-wing-it" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>There&rsquo;s one difference between models worth explaining: reasoning. The idea is that the model &ldquo;thinks before answering&rdquo; instead of generating tokens straight from left to right. Models with reasoning go through an internal chain-of-thought step where they evaluate the problem, consider alternatives, plan, and only then emit the response.</p>
<p>In practice this shows up as <code>&lt;think&gt;...&lt;/think&gt;</code> tags in the output, blocks of text the model writes to itself that shouldn&rsquo;t go to the end user. Claude Opus 4.6, GPT 5.4, DeepSeek V3.2 and the Qwen 3.5 line support reasoning natively. The smaller ones (Gemma 4, GPT OSS 20B, older models) don&rsquo;t have that capability.</p>
<p>Why does it matter for coding? When a coding agent gets &ldquo;build a Rails app with 9 components,&rdquo; it has to decompose the task into steps, decide the order, anticipate dependencies, adapt when something fails. Without reasoning, the model generates code sequentially with no planning. It works for simple tasks, falls apart on projects with interdependent parts.</p>
<p>In the benchmark, the difference was clear:</p>
<ul>
<li>GPT OSS 20B (no reasoning, 20B parameters) created the app in the wrong directory. Couldn&rsquo;t keep workspace instructions in mind while generating code.</li>
<li>Qwen 3 32B has reasoning, but at 7 tok/s it was too slow. The &ldquo;thinking&rdquo; tokens drag out the generation time.</li>
<li>Gemma 4 31B, with no reasoning trained for agentic use, fell into repetitive tool calling loops.</li>
<li>GLM 5 (cloud, 745B MoE) with reasoning and 44B active parameters, finished cleanly and used the correct API.</li>
</ul>
<p>There&rsquo;s a trade-off: reasoning consumes extra tokens (the <code>&lt;think&gt;</code> blocks), which take up VRAM in the KV Cache and slow generation down. That&rsquo;s why flags like <code>--reasoning-format none</code> are needed in llama.cpp. Some clients don&rsquo;t know what to do with reasoning tokens and break. Models that emit reasoning when the runtime isn&rsquo;t expecting it can produce garbage in the output.</p>
<p>And reasoning isn&rsquo;t something you &ldquo;turn on&rdquo; in any model. It&rsquo;s a capability trained with reinforcement learning on top of the base model, using data from problems that require multi-step thinking. The smaller open source models (20B-35B) typically didn&rsquo;t go through that training, or went through it on a smaller scale. They know how to generate code, but they don&rsquo;t know how to <em>plan</em> code. On tasks that require 50+ coordinated tool calls, that difference is fatal.</p>
<h2>The benchmark: methodology<span class="hx:absolute hx:-mt-20" id="the-benchmark-methodology"></span>
    <a href="#the-benchmark-methodology" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>To compare models fairly, I built an automated harness in Python. Each model gets the exact same prompt: build a complete Ruby on Rails application, a ChatGPT-style chat SPA using the RubyLLM gem, with Hotwire/Stimulus/Turbo Streams, Tailwind CSS, Minitest tests, CI tools (Brakeman, RuboCop, SimpleCov, bundle-audit), Dockerfile, docker-compose and README.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/05/llm-benchmark/crush-screenshot.png" alt="Crush — coding CLI from Charm"  loading="lazy" /></p>
<p>The runner is <a href="https://github.com/sst/opencode"target="_blank" rel="noopener">opencode</a>, one of the most popular open source coding CLIs, competing with Claude Code and Codex. One clarification worth making: the original author left the project to work on <a href="https://github.com/charmbracelet/crush"target="_blank" rel="noopener">Crush</a> together with the <a href="https://charm.sh/"target="_blank" rel="noopener">Charm</a> team (the folks behind Bubble Tea, Lip Gloss and several other Go terminal tools), but the rest of the original team kept evolving opencode normally — the project has not been discontinued. Today the two coexist. If you read <a href="/en/2026/01/09/omarchy-3-one-of-the-best-coding-agents-crush/">my piece on Crush</a>, you already know that branch. Both run everywhere: macOS, Linux, Windows, Android, FreeBSD.</p>
<p>I actually tried to use Crush for the benchmark first. The problem: it advertised a <code>--yolo</code> flag in its help to auto-approve every action (essential for unattended automated runs), but at runtime it rejected the flag. Without auto-approve there&rsquo;s no way to do an unattended benchmark. opencode, on the other hand, had the <code>opencode run --agent build --format json</code> mode that emits JSON events with session IDs and token counts, perfect for automation. So we went with opencode.</p>
<p>I picked opencode (and not Claude Code or Codex) for two reasons:</p>
<ol>
<li>Neutrality. Claude Code is optimized for Anthropic models. Codex is optimized for OpenAI models. opencode is agnostic, same interface for all.</li>
<li>Automation. opencode exposes a machine-readable JSON format. Claude Code and Codex don&rsquo;t have an equivalent interface for external benchmarking.</li>
</ol>
<p>Cloud models ran in two phases: phase 1 (build the app) and phase 2 (validate local boot, docker build, docker compose). Local models only ran phase 1.</p>
<p>Worth mentioning: the entire benchmark cost less than $10 in tokens on OpenRouter. Apart from GPT 5.4 Pro which torched $7.20 to fail, the other 11 cloud models added up to about $2.50 total. Local models cost only electricity. The point is: running your own benchmark is cheap. If you want to know whether a model works for your use case, drop the $2 and test it. The harness code is on GitHub, just swap the prompt for your own project.</p>
<h2>Why GPT 5.4 failed the benchmark (but not in real life)<span class="hx:absolute hx:-mt-20" id="why-gpt-54-failed-the-benchmark-but-not-in-real-life"></span>
    <a href="#why-gpt-54-failed-the-benchmark-but-not-in-real-life" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>GPT 5.4 Pro is the only cloud model that consistently failed our benchmark. Two separate runs, same result: the model generated files but never reached <code>finish_reason: stop</code>. It always ended on <code>finish_reason: tool-calls</code> — wanted to keep calling tools but the loop kept breaking.</p>
<p>For folks who don&rsquo;t know: tool calling is when an LLM needs to perform an action (read a file, run a command, edit code) and emits a &ldquo;tool call&rdquo; in a structured format. The client (opencode, Claude Code, Codex) interprets it, executes it, and returns the result back to the model. Each provider has its own format: Anthropic uses <code>tool_use</code> blocks, OpenAI uses <code>function_calling</code> with proprietary JSON schemas, Google uses <code>FunctionCall</code>.</p>
<p>GPT 5.4 is heavily trained for OpenAI&rsquo;s native function calling format — <code>tool_choice</code>, <code>tools</code> with proprietary JSON schemas. When the benchmark routes through opencode → OpenRouter → GPT 5.4, the tool schemas get translated at every hop. If GPT emits tool calls in a format that OpenRouter or opencode doesn&rsquo;t parse correctly, the agent loop breaks.</p>
<p>The evidence: every other cloud model (Claude Opus, Claude Sonnet, Kimi K2.5, DeepSeek V3.2, MiniMax M2.7, GLM 5, Qwen 3.6 Plus, Step 3.5 Flash) ended on <code>finish_reason: stop</code>. Only GPT ends on <code>finish_reason: tool-calls</code>.</p>
<p>A fair comparison for GPT 5.4 would require running it in its native environment. And now we have that comparison: we built support to automate the Codex CLI (<code>codex exec</code> with <code>--dangerously-bypass-approvals-and-sandbox</code> and reasoning effort <code>xhigh</code>) and ran the same benchmark. GPT 5.4 completed in 22 minutes, generated all 9 artifacts, wrote 22 tests with the most sophisticated architecture in the entire benchmark: dependency injection of the RubyLLM client, PORO models for <code>ChatMessage</code> and <code>PromptSubmission</code>, session-backed <code>ChatSession</code> with TTL and message trimming, bin/ci script.</p>
<p>But the code breaks on the second message. GPT 5.4 uses <code>chat.add_message(role:, content:)</code> with keyword arguments instead of a positional hash <code>chat.add_message({role:, content:})</code> — this causes <code>ArgumentError: wrong number of arguments (given 0, expected 1)</code> on the first multi-turn exchange. The first message works (uses <code>chat.ask</code> directly), multi-turn doesn&rsquo;t.</p>
<p>And the cost: <strong>7.6 million tokens</strong> on xHigh reasoning effort. That&rsquo;s 65x more tokens than Opus 4.7 (118K) for the same benchmark. Estimated cost of ~$16 per run, versus ~$1.10 for Opus. Spent 15x more and still got the calling convention wrong. A massive token budget and maximum reasoning effort still can&rsquo;t guarantee factual correctness on a gem API. API knowledge is binary recall in the weights, not a function of how hard the model &ldquo;thinks.&rdquo;</p>
<p>With objective data in hand, GPT 5.4 moves from Tier 1 to Tier 2. The architecture it generates is better than Opus in terms of design patterns. But the code needs a fix for multi-turn to work, and the per-token cost is prohibitive.</p>
<p>Sonnet and Opus through opencode/OpenRouter were probably also not pushed to their limits. Claude Code offers native tool support that opencode doesn&rsquo;t replicate — meaning the benchmark results represent a floor, not a ceiling, for those models.</p>
<h2>Open source models: reality vs the narrative<span class="hx:absolute hx:-mt-20" id="open-source-models-reality-vs-the-narrative"></span>
    <a href="#open-source-models-reality-vs-the-narrative" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A lot of people are saying open source models have already caught up with the commercial ones and you can run your own &ldquo;Claude&rdquo; at home. In practice, not really.</p>
<p>The scale isn&rsquo;t comparable. Frontier models like Claude Opus 4.6 and GPT 5.4 are closed-source, but estimates put them in the hundreds of billions to trillions of parameters range, trained with compute and data no open source company can replicate. The best models that fit on reasonable hardware are:</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th style="text-align: right">Total Parameters</th>
          <th style="text-align: right">Active Parameters</th>
          <th>Architecture</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Qwen 3.5 35B-A3B</td>
          <td style="text-align: right">35B</td>
          <td style="text-align: right">3B</td>
          <td>MoE (A3B)</td>
      </tr>
      <tr>
          <td>Qwen 3.5 27B</td>
          <td style="text-align: right">27B</td>
          <td style="text-align: right">27B</td>
          <td>Dense</td>
      </tr>
      <tr>
          <td>Qwen 3 32B</td>
          <td style="text-align: right">32B</td>
          <td style="text-align: right">32B</td>
          <td>Dense</td>
      </tr>
      <tr>
          <td>Qwen 3.5 122B</td>
          <td style="text-align: right">122B</td>
          <td style="text-align: right">122B</td>
          <td>Dense</td>
      </tr>
      <tr>
          <td>GPT OSS 20B</td>
          <td style="text-align: right">20B</td>
          <td style="text-align: right">20B</td>
          <td>Dense</td>
      </tr>
      <tr>
          <td>Gemma 4 31B</td>
          <td style="text-align: right">31B</td>
          <td style="text-align: right">31B</td>
          <td>Dense</td>
      </tr>
  </tbody>
</table>
<p>Post-publication correction: Qwen 3.5 35B is actually the <strong>35B-A3B</strong>, an MoE with only 3B active parameters per token (not dense, as I&rsquo;d originally written). That&rsquo;s why it runs relatively fast for its size. And for folks with 24 GB of VRAM, the model recommended by <a href="https://unsloth.ai/docs/models/qwen3.5#qwen3.5-27b"target="_blank" rel="noopener">Unsloth</a> themselves is the <strong>Qwen 3.5 27B</strong> dense — that one I didn&rsquo;t get around to testing in the benchmark, but it&rsquo;s worth a look. For anyone wanting to dig deeper into local models, <a href="https://x.com/sudoingX"target="_blank" rel="noopener">@sudoingX</a> has been doing some serious experimentation in this space. Thanks to <a href="https://x.com/thpmacedo/status/2041105305111502927"target="_blank" rel="noopener">@thpmacedo</a> for the heads-up.</p>
<p>Even the largest open source MoE (Mixture of Experts) models that companies make publicly available activate only a small fraction of parameters per token:</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th style="text-align: right">Total Parameters</th>
          <th style="text-align: right">Active Parameters</th>
          <th>Notes</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Kimi K2.5</td>
          <td style="text-align: right">1T</td>
          <td style="text-align: right">32B</td>
          <td>384 experts, top-8 + shared</td>
      </tr>
      <tr>
          <td>GLM 5</td>
          <td style="text-align: right">745B</td>
          <td style="text-align: right">44B</td>
          <td>256 experts, 8 activated</td>
      </tr>
      <tr>
          <td>DeepSeek V3.2</td>
          <td style="text-align: right">671B</td>
          <td style="text-align: right">37B</td>
          <td>Sparse Attention</td>
      </tr>
      <tr>
          <td>Qwen 3.5 397B</td>
          <td style="text-align: right">397B</td>
          <td style="text-align: right">17B</td>
          <td>MoE, cloud-only</td>
      </tr>
  </tbody>
</table>
<p>These large models aren&rsquo;t self-hostable. Kimi K2.5 with 1T parameters needs GPU clusters with hundreds of GBs of VRAM. GLM 5 with 745B is the same. Even if Alibaba or Z.AI release the weights (and some do), nobody has home hardware to run them.</p>
<p>What fits on your home GPU are the 20B-35B models — and those have real limitations.</p>
<h3>What each local model did in the benchmark<span class="hx:absolute hx:-mt-20" id="what-each-local-model-did-in-the-benchmark"></span>
    <a href="#what-each-local-model-did-in-the-benchmark" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Results from the original run on the AMD Strix Halo:</p>
<p><strong>Qwen 3 Coder Next (30B)</strong> — Completed in 17 minutes on the Strix, generated 1675 files, Rails app with all the artifacts. But only 3 tests. And more importantly: it invented <code>RubyLLM::Client.new</code>, a class that doesn&rsquo;t exist in the gem. The app doesn&rsquo;t run.</p>
<p><strong>Qwen 3.5 35B</strong> — Completed in 28 minutes on the Strix, 1478 files, 11 tests. Used <code>RubyLLM.chat</code> without a model parameter — works only if the default is configured. No LLM mocking in the tests.</p>
<p><strong>Qwen 3.5 122B</strong> — Completed in 43 minutes on the Strix, 1503 files, 16 tests. But it ignored the RubyLLM gem completely and built a custom HTTP client for OpenRouter. The prompt explicitly asked for ruby_llm.</p>
<p><strong>GLM 4.7 Flash (local, Strix)</strong> — Produced 2029 files with all the artifacts, but the session ended mid-tool-call. The cloud version (GLM 5) works perfectly.</p>
<p><strong>Gemma 4 31B (Strix)</strong> — Infinite tool call loop after ~11 productive steps. Known llama.cpp bug.</p>
<p><strong>GPT OSS 20B (Strix)</strong> — Created the Rails app in the wrong directory (<code>project/app/</code> instead of <code>project/</code>). A 20B model doesn&rsquo;t follow workspace instructions reliably.</p>
<p><strong>Qwen 3 32B (Strix)</strong> — Way too slow (7.3 tok/s). The hardware can&rsquo;t keep up.</p>
<p>And the results from the rerun on the NVIDIA RTX 5090 (all with Q3_K_M or Q4_K_M and contexts between 64k and 128k to fit the 32 GB of VRAM):</p>
<p><strong>Qwen 3.5 35B-A3B (5090)</strong> — 5 minutes at 273 tok/s. Recognizable Rails project, entry point <code>RubyLLM.chat(model:)</code> is right, but it hallucinates <code>chat.add_message(role:, content:)</code> and <code>chat.complete</code> instead of <code>.ask</code>. Fixable in 1-2 follow-ups. The best candidate for &ldquo;OSS local that&rsquo;s actually worth trying.&rdquo;</p>
<p><strong>Qwen 3.5 27B Claude-distilled (5090)</strong> — 12 minutes at 129 tok/s. Impeccable Claude style, total API hallucination (<code>RubyLLM::Chat.new.with_model{}</code>, <code>add_message</code>, <code>response.text</code>). More details in the distillation section below.</p>
<p><strong>Qwen 3 Coder 30B (5090)</strong> — 6 minutes at 145 tok/s. Returned a hardcoded mock string instead of calling the API. Tier 3 unusable.</p>
<p><strong>Qwen 2.5 Coder 32B (5090)</strong> — 90 minutes of timeout, zero files. The model spun without ever calling a write tool.</p>
<p><strong>Qwen 3 32B (5090)</strong> — 4 minutes at 69 tok/s, partial scaffold, errors. The general version is better than the Coder one but still breaks.</p>
<p><strong>Gemma 4 31B (5090)</strong> — 8 minutes at 213 tok/s. Same repetition loop it had on the Strix. The llama.cpp bug isn&rsquo;t a hardware issue.</p>
<p><strong>Qwen 3.5 27B Sushi Coder RL (5090)</strong> — Infrastructure failure (<code>ProviderModelNotFoundError</code>), couldn&rsquo;t be evaluated. Redo on a future run.</p>
<p><strong>GPT OSS 20B (5090)</strong> — Pulled from this run because of a recent llama.cpp main regression in the harmony family tool call parser. The logs show <code>Failed to parse input at pos 755: &lt;|channel|&gt;...</code> in multi-turn sessions. It worked on the Strix with llama.cpp <code>b8643</code>, broken on today&rsquo;s main. Waiting on upstream to fix it.</p>
<h2>Cloud models: what actually works<span class="hx:absolute hx:-mt-20" id="cloud-models-what-actually-works"></span>
    <a href="#cloud-models-what-actually-works" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Of the 12 models that completed the benchmark, all of them generated a recognizable Rails project with all the requested artifacts (Gemfile, routes, views, JS, tests, README, Dockerfile, docker-compose). 9 out of 9 on the completeness checklist.</p>
<p>But here comes the question that matters: does the code run?</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/05/llm-benchmark/en/cost-vs-quality.png" alt="Cost vs time — and does the code work?"  loading="lazy" /></p>
<p>The correct RubyLLM API is simple:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">chat</span> <span class="o">=</span> <span class="no">RubyLLM</span><span class="o">.</span><span class="n">chat</span><span class="p">(</span><span class="ss">model</span><span class="p">:</span> <span class="s2">&#34;anthropic/claude-sonnet-4&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">response</span> <span class="o">=</span> <span class="n">chat</span><span class="o">.</span><span class="n">ask</span><span class="p">(</span><span class="s2">&#34;Hello&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">response</span><span class="o">.</span><span class="n">content</span>  <span class="c1"># =&gt; &#34;Hi there!&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>8 of the 12 models invented APIs that don&rsquo;t exist. The most common pattern: hallucinating an interface that doesn&rsquo;t match the actual gem:</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th>What It Invented</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DeepSeek V3.2</td>
          <td><code>RubyLLM::Client.new</code> — nonexistent class</td>
      </tr>
      <tr>
          <td>Qwen 3 Coder Next</td>
          <td><code>RubyLLM::Client.new</code> — same error</td>
      </tr>
      <tr>
          <td>Qwen 3.5 122B</td>
          <td><code>Openrouter::Client</code> — nonexistent gem</td>
      </tr>
      <tr>
          <td>Kimi K2.5</td>
          <td><code>add_message()</code> and <code>complete()</code> — nonexistent methods</td>
      </tr>
      <tr>
          <td>MiniMax M2.7</td>
          <td><code>RubyLLM.chat(messages: [...])</code> — nonexistent signature</td>
      </tr>
      <tr>
          <td>Qwen 3.6 Plus</td>
          <td><code>chat.add_message()</code> — nonexistent method</td>
      </tr>
      <tr>
          <td>Gemini 3.1 Pro</td>
          <td><code>RubyLLM::Chat.new()</code> and <code>add_message()</code> — internal API, not public</td>
      </tr>
      <tr>
          <td>Grok 4.20</td>
          <td>Ignores the gem completely — uses <code>OpenAI::Client</code> (ruby-openai) hitting the OpenRouter URL directly</td>
      </tr>
  </tbody>
</table>
<p>The models that got it right — both Claudes, GLM 5 and GLM 5.1 — used the simple two-step pattern (<code>chat = RubyLLM.chat(model:)</code> then <code>chat.ask(message)</code>). The ones that got it wrong tried to make RubyLLM look like the OpenAI Python SDK, which is a different thing. Grok 4.20 was the most brazen case: it didn&rsquo;t even try to use the gem, it went straight for <code>OpenAI::Client</code> pointing at the OpenRouter URL, ignoring the explicit prompt.</p>
<p>And the tests? Only Opus, Sonnet, GLM 5 and GLM 5.1 did proper mocking of the LLM calls. All the others either hit the real API (which fails without a key) or mocked the invented API (tests pass but prove nothing). Test count is a misleading metric: Kimi K2.5 wrote 37 tests, more than anyone else, but none of them test real functionality because the API it uses doesn&rsquo;t exist.</p>
<h3>Real viability table<span class="hx:absolute hx:-mt-20" id="real-viability-table"></span>
    <a href="#real-viability-table" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><table>
  <thead>
      <tr>
          <th>Model</th>
          <th style="text-align: center">Correct API?</th>
          <th style="text-align: center">Runs?</th>
          <th style="text-align: center">Test Mocking?</th>
          <th>Problem</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Claude Opus 4.7</strong></td>
          <td style="text-align: center">Yes</td>
          <td style="text-align: center"><strong>Yes</strong></td>
          <td style="text-align: center">Yes (FakeChat)</td>
          <td>Clean implementation, 28 tests</td>
      </tr>
      <tr>
          <td><strong>Claude Sonnet 4.6</strong></td>
          <td style="text-align: center">Yes</td>
          <td style="text-align: center"><strong>Yes</strong></td>
          <td style="text-align: center">Yes (mocha)</td>
          <td>Clean implementation</td>
      </tr>
      <tr>
          <td><strong>Claude Opus 4.6</strong></td>
          <td style="text-align: center">Yes</td>
          <td style="text-align: center"><strong>Yes</strong></td>
          <td style="text-align: center">Yes (mocha)</td>
          <td>Clean implementation</td>
      </tr>
      <tr>
          <td><strong>GLM 5</strong></td>
          <td style="text-align: center">Yes</td>
          <td style="text-align: center"><strong>Yes</strong></td>
          <td style="text-align: center">Yes (mocha)</td>
          <td>Correct API, works</td>
      </tr>
      <tr>
          <td><strong>GLM 5.1</strong></td>
          <td style="text-align: center">Yes</td>
          <td style="text-align: center"><strong>Yes</strong></td>
          <td style="text-align: center">Yes</td>
          <td>Correct API, works</td>
      </tr>
      <tr>
          <td>GPT 5.4 (Codex)</td>
          <td style="text-align: center">Partial</td>
          <td style="text-align: center">1st msg only</td>
          <td style="text-align: center">Yes (FakeChat)</td>
          <td><code>add_message(role:, content:)</code> with keyword args instead of positional hash — breaks multi-turn</td>
      </tr>
      <tr>
          <td>Step 3.5 Flash</td>
          <td style="text-align: center">N/A</td>
          <td style="text-align: center"><strong>Yes</strong>*</td>
          <td style="text-align: center">No</td>
          <td>Bypasses RubyLLM, uses HTTP directly</td>
      </tr>
      <tr>
          <td>Grok 4.20</td>
          <td style="text-align: center">N/A</td>
          <td style="text-align: center"><strong>Yes</strong>*</td>
          <td style="text-align: center">No</td>
          <td>Bypasses RubyLLM, uses <code>OpenAI::Client</code> directly</td>
      </tr>
      <tr>
          <td>Qwen 3.6 Plus</td>
          <td style="text-align: center">Partial</td>
          <td style="text-align: center">Only 1st msg</td>
          <td style="text-align: center">No</td>
          <td><code>add_message()</code> doesn&rsquo;t exist</td>
      </tr>
      <tr>
          <td>Qwen 3.5 35B</td>
          <td style="text-align: center">Partial</td>
          <td style="text-align: center">Maybe</td>
          <td style="text-align: center">No</td>
          <td>No model parameter</td>
      </tr>
      <tr>
          <td>Kimi K2.5</td>
          <td style="text-align: center">No</td>
          <td style="text-align: center"><strong>No</strong></td>
          <td style="text-align: center">No</td>
          <td><code>add_message()</code>/<code>complete()</code> invented</td>
      </tr>
      <tr>
          <td>MiniMax M2.7</td>
          <td style="text-align: center">No</td>
          <td style="text-align: center"><strong>No</strong></td>
          <td style="text-align: center">No</td>
          <td><code>RubyLLM.chat</code> signature wrong</td>
      </tr>
      <tr>
          <td>DeepSeek V3.2</td>
          <td style="text-align: center">No</td>
          <td style="text-align: center"><strong>No</strong></td>
          <td style="text-align: center">No</td>
          <td><code>RubyLLM::Client</code> nonexistent</td>
      </tr>
      <tr>
          <td>Qwen 3 Coder Next</td>
          <td style="text-align: center">No</td>
          <td style="text-align: center"><strong>No</strong></td>
          <td style="text-align: center">No</td>
          <td><code>RubyLLM::Client</code> nonexistent</td>
      </tr>
      <tr>
          <td>Gemini 3.1 Pro</td>
          <td style="text-align: center">No</td>
          <td style="text-align: center"><strong>No</strong></td>
          <td style="text-align: center">Wrong mock</td>
          <td><code>RubyLLM::Chat.new()</code> and <code>add_message()</code> don&rsquo;t exist</td>
      </tr>
      <tr>
          <td>Qwen 3.5 122B</td>
          <td style="text-align: center">No</td>
          <td style="text-align: center"><strong>No</strong></td>
          <td style="text-align: center">No</td>
          <td><code>Openrouter::Client</code> gem doesn&rsquo;t exist</td>
      </tr>
  </tbody>
</table>
<p>*Step 3.5 Flash works by calling the OpenRouter REST API directly with <code>Net::HTTP</code>, completely bypassing the gem the prompt asked for.</p>
<p>Now, this doesn&rsquo;t mean those models are useless. If you take Kimi K2.5 or DeepSeek V3.2 and tell it &ldquo;the RubyLLM::Client class doesn&rsquo;t exist, fix it to use the gem&rsquo;s real API&rdquo;, it&rsquo;ll probably fix it. One or two follow-ups and the project becomes functional. Most of the models that failed here could deliver a working project with a few more rounds of conversation.</p>
<p>But that&rsquo;s where the trade-off lives. With Opus or GPT 5.4, the first output already works. You ask, they deliver, you test, it runs. With the cheaper models, you&rsquo;ll spend time fixing API hallucinations, debugging code that &ldquo;looks right&rdquo; but crashes, steering the model in the right direction. Each of those rounds is 10-30 minutes. Three extra rounds and you&rsquo;ve spent an hour of your time to save $0.90 in tokens.</p>
<p>You save dollars, you spend time. And time is money. For someone learning or exploring without urgency, that trade can make sense. For someone who needs to ship, the frontier models pay for themselves fast.</p>
<h3>Comparing the models that work<span class="hx:absolute hx:-mt-20" id="comparing-the-models-that-work"></span>
    <a href="#comparing-the-models-that-work" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><table>
  <thead>
      <tr>
          <th>Model</th>
          <th>Provider</th>
          <th style="text-align: right">Time</th>
          <th style="text-align: right">Tests</th>
          <th style="text-align: right">Cost/Run</th>
          <th>vs Opus</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Claude Opus 4.7</td>
          <td>OpenRouter</td>
          <td style="text-align: right">18m</td>
          <td style="text-align: right">28</td>
          <td style="text-align: right">~$1.10</td>
          <td>New baseline</td>
      </tr>
      <tr>
          <td>Claude Sonnet 4.6</td>
          <td>OpenRouter</td>
          <td style="text-align: right">16m</td>
          <td style="text-align: right">30</td>
          <td style="text-align: right">~$0.63</td>
          <td>40% cheaper, more tests</td>
      </tr>
      <tr>
          <td>Claude Opus 4.6</td>
          <td>OpenRouter</td>
          <td style="text-align: right">16m</td>
          <td style="text-align: right">16</td>
          <td style="text-align: right">~$1.05</td>
          <td>Previous baseline</td>
      </tr>
      <tr>
          <td>GLM 5</td>
          <td>OpenRouter</td>
          <td style="text-align: right">17m</td>
          <td style="text-align: right">7</td>
          <td style="text-align: right">~$0.11</td>
          <td>89% cheaper</td>
      </tr>
      <tr>
          <td>GLM 5.1</td>
          <td>Z.AI direct</td>
          <td style="text-align: right">22m</td>
          <td style="text-align: right">24</td>
          <td style="text-align: right">~$0.13</td>
          <td>~88% cheaper, more tests than GLM 5</td>
      </tr>
  </tbody>
</table>
<h3>Full ranking by time and tokens<span class="hx:absolute hx:-mt-20" id="full-ranking-by-time-and-tokens"></span>
    <a href="#full-ranking-by-time-and-tokens" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/05/llm-benchmark/en/time-to-complete.png" alt="Time to complete by model"  loading="lazy" /></p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th>Provider</th>
          <th style="text-align: right">Time</th>
          <th style="text-align: right">Total Tokens</th>
          <th style="text-align: right">Tok/s</th>
          <th style="text-align: right">Cost/Run</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Grok 4.20</td>
          <td>OpenRouter</td>
          <td style="text-align: right">8m</td>
          <td style="text-align: right">63,457</td>
          <td style="text-align: right">412.54</td>
          <td style="text-align: right">~$0.04</td>
      </tr>
      <tr>
          <td>Gemini 3.1 Pro</td>
          <td>OpenRouter</td>
          <td style="text-align: right">14m</td>
          <td style="text-align: right">104,034</td>
          <td style="text-align: right">128.28</td>
          <td style="text-align: right">~$0.50</td>
      </tr>
      <tr>
          <td>MiniMax M2.7</td>
          <td>OpenRouter</td>
          <td style="text-align: right">14m</td>
          <td style="text-align: right">79,743</td>
          <td style="text-align: right">574.52</td>
          <td style="text-align: right">~$0.05</td>
      </tr>
      <tr>
          <td>Claude Opus 4.6</td>
          <td>OpenRouter</td>
          <td style="text-align: right">16m</td>
          <td style="text-align: right">136,806</td>
          <td style="text-align: right">347.18</td>
          <td style="text-align: right">~$1.05</td>
      </tr>
      <tr>
          <td>Claude Sonnet 4.6</td>
          <td>OpenRouter</td>
          <td style="text-align: right">16m</td>
          <td style="text-align: right">127,067</td>
          <td style="text-align: right">532.26</td>
          <td style="text-align: right">~$0.63</td>
      </tr>
      <tr>
          <td>GLM 5</td>
          <td>OpenRouter</td>
          <td style="text-align: right">17m</td>
          <td style="text-align: right">59,378</td>
          <td style="text-align: right">400.01</td>
          <td style="text-align: right">~$0.11</td>
      </tr>
      <tr>
          <td>Qwen 3.6 Plus</td>
          <td>OpenRouter</td>
          <td style="text-align: right">17m</td>
          <td style="text-align: right">88,940</td>
          <td style="text-align: right">182.91</td>
          <td style="text-align: right">Free</td>
      </tr>
      <tr>
          <td>Claude Opus 4.7</td>
          <td>OpenRouter</td>
          <td style="text-align: right">18m</td>
          <td style="text-align: right">118,216</td>
          <td style="text-align: right">328.24</td>
          <td style="text-align: right">~$1.10</td>
      </tr>
      <tr>
          <td>GLM 5.1</td>
          <td>Z.AI direct</td>
          <td style="text-align: right">22m</td>
          <td style="text-align: right">81,666</td>
          <td style="text-align: right">166.62</td>
          <td style="text-align: right">~$0.13</td>
      </tr>
      <tr>
          <td>GPT 5.4 (Codex)</td>
          <td>Codex CLI</td>
          <td style="text-align: right">22m</td>
          <td style="text-align: right">7,643,800</td>
          <td style="text-align: right">5,824.56</td>
          <td style="text-align: right">~$16.00</td>
      </tr>
      <tr>
          <td>Qwen 3 Coder Next</td>
          <td>Local</td>
          <td style="text-align: right">17m</td>
          <td style="text-align: right">39,054</td>
          <td style="text-align: right">37.49</td>
          <td style="text-align: right">Electricity</td>
      </tr>
      <tr>
          <td>Qwen 3.5 35B</td>
          <td>Local</td>
          <td style="text-align: right">28m</td>
          <td style="text-align: right">76,919</td>
          <td style="text-align: right">46.03</td>
          <td style="text-align: right">Electricity</td>
      </tr>
      <tr>
          <td>Kimi K2.5</td>
          <td>OpenRouter</td>
          <td style="text-align: right">29m</td>
          <td style="text-align: right">63,638</td>
          <td style="text-align: right">160.14</td>
          <td style="text-align: right">~$0.07</td>
      </tr>
      <tr>
          <td>Step 3.5 Flash</td>
          <td>OpenRouter</td>
          <td style="text-align: right">38m</td>
          <td style="text-align: right">156,267</td>
          <td style="text-align: right">242.11</td>
          <td style="text-align: right">~$0.02</td>
      </tr>
      <tr>
          <td>Qwen 3.5 122B</td>
          <td>Local</td>
          <td style="text-align: right">43m</td>
          <td style="text-align: right">57,472</td>
          <td style="text-align: right">22.41</td>
          <td style="text-align: right">Electricity</td>
      </tr>
      <tr>
          <td>DeepSeek V3.2</td>
          <td>OpenRouter</td>
          <td style="text-align: right">60m</td>
          <td style="text-align: right">115,278</td>
          <td style="text-align: right">53.37</td>
          <td style="text-align: right">~$0.04</td>
      </tr>
  </tbody>
</table>
<p>DeepSeek V3.2 is the slowest despite being cloud — it has no prompt caching, so it resends the full context on every turn.</p>
<h3>Token efficiency and cache<span class="hx:absolute hx:-mt-20" id="token-efficiency-and-cache"></span>
    <a href="#token-efficiency-and-cache" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Models with prompt caching pay much less in effective tokens:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/05/llm-benchmark/en/token-efficiency.png" alt="Token efficiency: cache vs new"  loading="lazy" /></p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th style="text-align: right">Total Tokens</th>
          <th style="text-align: right">Cache Read</th>
          <th style="text-align: right">Effective New Tokens</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Claude Sonnet 4.6</td>
          <td style="text-align: right">127,067</td>
          <td style="text-align: right">126,429</td>
          <td style="text-align: right">638</td>
      </tr>
      <tr>
          <td>Claude Opus 4.7</td>
          <td style="text-align: right">118,216</td>
          <td style="text-align: right">116,824</td>
          <td style="text-align: right">1,392</td>
      </tr>
      <tr>
          <td>Claude Opus 4.6</td>
          <td style="text-align: right">136,806</td>
          <td style="text-align: right">135,976</td>
          <td style="text-align: right">830</td>
      </tr>
      <tr>
          <td>GLM 5</td>
          <td style="text-align: right">59,378</td>
          <td style="text-align: right">58,240</td>
          <td style="text-align: right">1,138</td>
      </tr>
      <tr>
          <td>GLM 5.1</td>
          <td style="text-align: right">81,666</td>
          <td style="text-align: right">81,216</td>
          <td style="text-align: right">450</td>
      </tr>
      <tr>
          <td>Grok 4.20</td>
          <td style="text-align: right">63,457</td>
          <td style="text-align: right">62,400</td>
          <td style="text-align: right">1,057</td>
      </tr>
      <tr>
          <td>Gemini 3.1 Pro</td>
          <td style="text-align: right">104,034</td>
          <td style="text-align: right">98,129</td>
          <td style="text-align: right">5,905</td>
      </tr>
      <tr>
          <td>GPT 5.4 (Codex)</td>
          <td style="text-align: right">7,643,800</td>
          <td style="text-align: right">0</td>
          <td style="text-align: right">7,643,800</td>
      </tr>
      <tr>
          <td>DeepSeek V3.2</td>
          <td style="text-align: right">115,278</td>
          <td style="text-align: right">0</td>
          <td style="text-align: right">115,278</td>
      </tr>
      <tr>
          <td>Kimi K2.5</td>
          <td style="text-align: right">63,638</td>
          <td style="text-align: right">0</td>
          <td style="text-align: right">63,638</td>
      </tr>
  </tbody>
</table>
<h2>Speed: the chasm between cloud and local<span class="hx:absolute hx:-mt-20" id="speed-the-chasm-between-cloud-and-local"></span>
    <a href="#speed-the-chasm-between-cloud-and-local" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>There&rsquo;s an aspect that the cost tables hide: inference speed. And the difference is brutal.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/05/llm-benchmark/en/speed-comparison.png" alt="Inference speed by model"  loading="lazy" /></p>
<p>Claude Sonnet generates 532 tok/s. Qwen 3.5 122B running locally on my Minisforum (AMD Strix Halo) generates 22 tok/s. That&rsquo;s a 24x difference. In practice, what Sonnet does in 16 minutes, Qwen 3.5 122B takes 43 minutes. Qwen 3 Coder Next at 37 tok/s is the fastest of the local models on the Strix and even so it&rsquo;s 14x slower than Sonnet.</p>
<p>And it&rsquo;s not just clock time. When you&rsquo;re in an interactive coding loop — ask for a change, wait for output, test, ask for another — the model&rsquo;s speed sets your rhythm. At 37 tok/s, every long response makes you wait 30-60 seconds. At 530 tok/s, it appears almost instantly. Over a day, you feel it.</p>
<p>DeepSeek V3.2 is a curious case: it&rsquo;s cloud but it runs at 53 tok/s, slower than the locally-running Qwen 3.5 35B on the Strix (46 tok/s). The reason is that DeepSeek has no prompt caching — it resends the full context on every turn, strangling throughput. Paying for a cloud model that&rsquo;s slower than running it locally doesn&rsquo;t make any sense.</p>
<p>Local models are free in tokens, but they pay in time. On the AMD Strix, that math was a non-starter for every Qwen I tested: two minutes waiting for a long response, multiplied by 50 turns, eats your whole afternoon. But that changes when the hardware changes, and that&rsquo;s why I redid the local part of the benchmark on a different machine.</p>
<h2>AMD Strix Halo vs NVIDIA RTX 5090: what changes when the memory bandwidth doubles<span class="hx:absolute hx:-mt-20" id="amd-strix-halo-vs-nvidia-rtx-5090-what-changes-when-the-memory-bandwidth-doubles"></span>
    <a href="#amd-strix-halo-vs-nvidia-rtx-5090-what-changes-when-the-memory-bandwidth-doubles" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>To check whether the bottleneck was hardware or model, I took the same Qwen models and reran the benchmark on a workstation with an NVIDIA RTX 5090 (Blackwell, 32 GB GDDR7, 1,792 GB/s bandwidth). The numbers shift in a way that&rsquo;s worth looking at carefully.</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th style="text-align: right">AMD Strix (LPDDR5x)</th>
          <th style="text-align: right">NVIDIA 5090 (GDDR7)</th>
          <th style="text-align: right">Speedup</th>
          <th style="text-align: right">Total time on 5090</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Qwen 3 32B (dense)</td>
          <td style="text-align: right">7 tok/s</td>
          <td style="text-align: right">69 tok/s</td>
          <td style="text-align: right">~10x</td>
          <td style="text-align: right">4 min</td>
      </tr>
      <tr>
          <td>Qwen 3 Coder 30B (Coder)</td>
          <td style="text-align: right">37 tok/s</td>
          <td style="text-align: right">145 tok/s</td>
          <td style="text-align: right">~4x</td>
          <td style="text-align: right">6 min</td>
      </tr>
      <tr>
          <td>Qwen 3.5 35B-A3B (MoE)</td>
          <td style="text-align: right">46 tok/s</td>
          <td style="text-align: right"><strong>273 tok/s</strong></td>
          <td style="text-align: right">~6x</td>
          <td style="text-align: right">5 min</td>
      </tr>
      <tr>
          <td>Qwen 3.5 27B Claude (distilled)</td>
          <td style="text-align: right">timeout 90m</td>
          <td style="text-align: right">129 tok/s</td>
          <td style="text-align: right">n/a</td>
          <td style="text-align: right">12 min</td>
      </tr>
      <tr>
          <td>Gemma 4 31B</td>
          <td style="text-align: right">(didn&rsquo;t test on Strix)</td>
          <td style="text-align: right">213 tok/s</td>
          <td style="text-align: right">n/a</td>
          <td style="text-align: right">8 min</td>
      </tr>
      <tr>
          <td>Qwen 2.5 Coder 32B</td>
          <td style="text-align: right">(didn&rsquo;t test on Strix)</td>
          <td style="text-align: right">2.86 tok/s</td>
          <td style="text-align: right">n/a</td>
          <td style="text-align: right">timeout 90m</td>
      </tr>
  </tbody>
</table>
<p>To put those speeds in context, remember that in the cloud Sonnet runs at 532 tok/s, Opus at 347 tok/s, Step 3.5 Flash at 242 tok/s, Gemini 3.1 Pro at 128 tok/s and Kimi K2.5 at 160 tok/s. Qwen 3.5 35B-A3B on the 5090, at 273 tok/s, is in the same neighborhood as Step 3.5 Flash, faster than Gemini, Kimi and GLM 5.1. Qwen 3 Coder 30B at 145 tok/s is in Gemini territory. The classic line &ldquo;local models are ten times slower than cloud&rdquo; stopped being true the moment the 5090 entered the conversation.</p>
<p>The practical consequence is that the &ldquo;time is money&rdquo; argument shifts. On the Strix, &ldquo;waiting an hour for a Qwen 3.5 122B to do what Sonnet does in 16 minutes&rdquo; is straight-up loss. On the 5090, waiting 5 minutes for Qwen 3.5 35B-A3B to do the work, plus 10-15 minutes for you to do 1-2 correction prompts, gives you a total in the 20-25 minute range. Sonnet does it in 16 minutes with zero corrections. The difference shrank enough that, if cost matters a lot, it&rsquo;s worth it.</p>
<p>The catch: for this to be worth it, the model has to be close enough to the right answer that 1-2 correction prompts can fix it. When the error is &ldquo;the model decided not to use the gem I asked for and returned a hardcoded mock string,&rdquo; like Qwen 3 Coder 30B did, no easy correction prompt fixes that. That&rsquo;s a redo.</p>
<h3>Before you spend money on hardware thinking it&rsquo;s the answer<span class="hx:absolute hx:-mt-20" id="before-you-spend-money-on-hardware-thinking-its-the-answer"></span>
    <a href="#before-you-spend-money-on-hardware-thinking-its-the-answer" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>I&rsquo;ve got to give a warning here, because it&rsquo;s the most common buying mistake I see right now. Every other week somebody tells me they&rsquo;re going to grab a Ryzen AI Max because it has 128 GB of unified memory and that &ldquo;lets you run huge models.&rdquo; Technically, sure — the model fits. In practice, it&rsquo;s almost unusable. The memory is LPDDR5x at 256 GB/s, seven times slower than the 5090&rsquo;s GDDR7. What fits doesn&rsquo;t run at human speed. My own Strix with Qwen 3.5 122B hit 22 tok/s and the run took 43 minutes. To do anything serious day to day, that&rsquo;s not workable.</p>
<p>The 5090 is clearly superior, and it starts to make sense even for smaller models precisely because of the memory bandwidth. A Mac Studio with high-speed unified memory (up to 800 GB/s on the M4 Ultra) is the other viable option, and costs proportionally the same. But neither of those comes anywhere close to beating the commercial models on quality — and the per-token price of Claude, GPT or GLM, combined with their brutal inference speed, makes the math hard to justify for anyone who isn&rsquo;t an enthusiast or a researcher. Expensive local AI hardware is a weekend hobby, a tool for people who need to run offline for compliance reasons, or a research playground. For day-after-day production work, right now, cloud is still the rational choice. A 128 GB Ryzen AI Max may look tempting on the spec sheet, but if the goal is serious coding agent work, it&rsquo;s money badly spent.</p>
<h2>The Qwen family: Coder vs General, distillation, and why nothing is a silver bullet<span class="hx:absolute hx:-mt-20" id="the-qwen-family-coder-vs-general-distillation-and-why-nothing-is-a-silver-bullet"></span>
    <a href="#the-qwen-family-coder-vs-general-distillation-and-why-nothing-is-a-silver-bullet" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>With so many different Qwens running in this rerun, it&rsquo;s worth doing a more focused analysis. What I learned might surprise people who follow model benchmarks on Twitter.</p>
<h3>Before getting to the results: what quantization is and what distillation is<span class="hx:absolute hx:-mt-20" id="before-getting-to-the-results-what-quantization-is-and-what-distillation-is"></span>
    <a href="#before-getting-to-the-results-what-quantization-is-and-what-distillation-is" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>These two concepts come up constantly in this discussion and they deserve a quick explanation.</p>
<p><strong>Quantization</strong> is the technique of compressing the model&rsquo;s weights so they take up less memory. A model trained in FP16 (16 bits per weight) can be quantized to Q8 (8 bits), Q4 (4 bits), Q3_K_M (3 bits, but with medium-sized groupings), and so on. Each step halves the size of the model on disk and in VRAM, at the cost of some loss of precision. Q8 is practically lossless. Q4 already loses something measurable. Q3 loses more. Q2 is the line where the model starts saying real nonsense. The rule of thumb is that for coding and multi-step reasoning, you want to stay at Q4 or higher. Q3_K_M is the minimum that still works for many models, and it&rsquo;s what fits a 27B on the 5090 with 128k context.</p>
<p>The surprise from my test, and look, this goes against the consensus, is that quantization wasn&rsquo;t the bottleneck here. I ran the Qwen 3.5 27B Claude-distilled in two versions: Q8 on the AMD Strix (~27 GB of weights) and Q3_K_M on the 5090 (~12 GB of weights). Both hallucinated exactly the same fake RubyLLM APIs. Q3_K_M even produced a cleaner Gemfile. The model&rsquo;s limitation was in what those weights know, not in the precision they were compressed to.</p>
<p><strong>Distillation</strong> is the technique of training a smaller model (the &ldquo;student&rdquo;) to imitate the output or behavior of a larger model (the &ldquo;teacher&rdquo;). The classic version is logit distillation — the student learns to approximate the teacher&rsquo;s probability distributions. The modern, more popular version for coding agents is distillation of <strong>reasoning traces</strong>: you take chain-of-thought from the big model on real problems and train the smaller one to reproduce the same reasoning style.</p>
<p>The hype of the moment is distilling Claude and GPT into open source models. The promise is that you can have &ldquo;Claude-at-home&rdquo; running locally. I wanted to test this, and that&rsquo;s why I added <a href="https://huggingface.co/Jackrong/Qwen3.5-27B-Claude-4.6-Opus-Reasoning-Distilled"target="_blank" rel="noopener">Jackrong&rsquo;s Qwen 3.5 27B distilled from Claude 4.6 Opus</a> to the benchmark. If any open source model was going to use RubyLLM correctly, this was the bet — after all, in the entire benchmark, Claude and GLM 5 are the only ones that get the API right.</p>
<h3>What the Claude-distilled learned (and what it didn&rsquo;t)<span class="hx:absolute hx:-mt-20" id="what-the-claude-distilled-learned-and-what-it-didnt"></span>
    <a href="#what-the-claude-distilled-learned-and-what-it-didnt" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>I ran the same distillation twice: once at Q8 on the AMD Strix (which blew through the 90-minute timeout), and once at Q3_K_M on the 5090 (completed in 12 minutes). Both produced the same elegant frustration.</p>
<p>The code that comes out looks like Claude. It has <code># frozen_string_literal: true</code> at the top of every file. It has a separate <code>Response</code> class as a value object with explicit attribute readers. It has a clear separation between service, controller and model. It has doc comments at the top of every file. It correctly comments out things like <code>active_record</code>, <code>active_job</code> and <code>action_mailer</code> in <code>application.rb</code>. It has defensive <code>case</code> statements trying multiple return formats. Stylistically, it&rsquo;s Claude.</p>
<p>Functionally, it&rsquo;s a complete RubyLLM hallucination. Look at the service generated by the 5090 run:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="no">RubyLLM</span><span class="o">::</span><span class="no">Chat</span><span class="o">.</span><span class="n">new</span><span class="o">.</span><span class="n">with_model</span><span class="p">(</span><span class="vi">@model</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">chat</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="n">conversation_history</span><span class="o">.</span><span class="n">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">msg</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">    <span class="n">chat</span><span class="o">.</span><span class="n">add_message</span><span class="p">(</span><span class="ss">role</span><span class="p">:</span> <span class="ss">:user</span><span class="p">,</span> <span class="ss">content</span><span class="p">:</span> <span class="n">msg</span><span class="o">[</span><span class="ss">:content</span><span class="o">]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="n">response</span> <span class="o">=</span> <span class="n">chat</span><span class="o">.</span><span class="n">ask</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="no">Response</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="ss">content</span><span class="p">:</span> <span class="n">response</span><span class="o">.</span><span class="n">text</span><span class="p">,</span> <span class="ss">usage</span><span class="p">:</span> <span class="n">build_usage</span><span class="p">(</span><span class="n">response</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Every primitive in this code is invented:</p>
<ul>
<li><code>RubyLLM::Chat.new</code> — the constructor isn&rsquo;t public, the correct entry is <code>RubyLLM.chat(model:)</code></li>
<li><code>.with_model(@model) do |chat| ... end</code> — there&rsquo;s no block API like that</li>
<li><code>chat.add_message(role:, content:)</code> — doesn&rsquo;t exist</li>
<li><code>response.text</code> — the real API exposes <code>response.content</code></li>
<li><code>response.usage.prompt_tokens</code> — the object doesn&rsquo;t have that shape</li>
</ul>
<p>This will blow up with a <code>NoMethodError</code> on the first request. The initializer also tries <code>config.openrouter_api_base=</code> which doesn&rsquo;t exist on <code>RubyLLM.configure</code>, so the app probably won&rsquo;t even boot.</p>
<p>The Q8 version on the AMD Strix does the exact same thing, with one difference: the entry call is <code>RubyLLM.chat(model:, provider: :openrouter)</code> — the entry point is right, but <code>provider:</code> is invented and it&rsquo;s immediately followed by the same fake <code>chat.add_message(role:, content:)</code>. Worse, the Gemfile from the 90-minute run lists <code>gem &quot;ruby-openai&quot;</code> (wrong gem!), <code>gem &quot;minitest&quot;, &quot;~&gt; 6.0&quot;</code> (minitest 6.0 doesn&rsquo;t exist) and <code>gem &quot;tailwindcss&quot;</code> (wrong gem name, it&rsquo;s <code>tailwindcss-rails</code>). The Gemfile doesn&rsquo;t include the gem the service code itself is trying to use.</p>
<p>For comparison, look at the actual Claude Opus 4.6 baseline, in the same benchmark, getting it all right:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="vi">@chat</span> <span class="o">=</span> <span class="no">RubyLLM</span><span class="o">.</span><span class="n">chat</span><span class="p">(</span><span class="ss">model</span><span class="p">:</span> <span class="n">model_id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">response</span> <span class="o">=</span> <span class="vi">@chat</span><span class="o">.</span><span class="n">ask</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">response</span><span class="o">.</span><span class="n">content</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Twelve lines in the entire service. Zero hallucination. Includes streaming via block. The distilled model produced three times the code volume and got the API wrong.</p>
<p>The honest reading is that distillation transferred one layer and stopped. The layer that came along was the style: code organization, comments, class structure, the order of things. The layer that got left behind was factual memory about specific libraries. That makes sense when you think about it: Claude&rsquo;s reasoning traces, even when written carefully, rarely contain repeated references to <code>chat.ask(msg).content</code> in some obscure Ruby gem. The student only learns what the teacher repeats, and Claude never had any reason to keep whispering &ldquo;use ask, not complete&rdquo; throughout its chains of thought. Library API knowledge is binary recall memory, the kind that&rsquo;s either in the weights or it isn&rsquo;t. Decomposing that into reasoning steps is impossible because it isn&rsquo;t reasoning, it&rsquo;s just raw memorization.</p>
<p>To wrap up the practical recommendation: if you need the model to actually use RubyLLM, or any less-popular library for that matter, Claude distillation won&rsquo;t save you. Use real Claude or GLM 5. The &ldquo;Claude-stand-ins&rdquo; in open source will fail the same way the Qwen base would, just with prettier handwriting.</p>
<h3>Coder vs General: the surprise of the &ldquo;for coding&rdquo; models<span class="hx:absolute hx:-mt-20" id="coder-vs-general-the-surprise-of-the-for-coding-models"></span>
    <a href="#coder-vs-general-the-surprise-of-the-for-coding-models" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Almost everyone&rsquo;s instinct is that models with &ldquo;Coder&rdquo; in the name are the best for programming. Makes sense, they were specifically fine-tuned on code. But in the benchmark, it was exactly the opposite.</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th>Type</th>
          <th>Hardware</th>
          <th style="text-align: right">Time</th>
          <th>Result</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Qwen 3.5 35B-A3B</td>
          <td>General (MoE)</td>
          <td>5090</td>
          <td style="text-align: right">5 min</td>
          <td>Runs Rails, hallucinates <code>add_message</code>/<code>complete</code> (1-2 follow-ups fix it)</td>
      </tr>
      <tr>
          <td>Qwen 3 Coder 30B</td>
          <td>Coder</td>
          <td>5090</td>
          <td style="text-align: right">6 min</td>
          <td>Returned a hardcoded mock string instead of calling RubyLLM</td>
      </tr>
      <tr>
          <td>Qwen 2.5 Coder 32B</td>
          <td>Coder</td>
          <td>5090</td>
          <td style="text-align: right">timeout 90m</td>
          <td>Zero files, model froze</td>
      </tr>
      <tr>
          <td>Qwen 3 32B</td>
          <td>General</td>
          <td>5090</td>
          <td style="text-align: right">4 min</td>
          <td>Partial scaffold, errors</td>
      </tr>
      <tr>
          <td>Qwen 3.5 27B Claude-distilled</td>
          <td>General + distilled</td>
          <td>5090</td>
          <td style="text-align: right">12 min</td>
          <td>Runs Rails, hallucinates the entire API</td>
      </tr>
      <tr>
          <td>Qwen 3.5 27B Sushi Coder RL</td>
          <td>Coder (RL)</td>
          <td>5090</td>
          <td style="text-align: right">6 min</td>
          <td>Infrastructure failure, couldn&rsquo;t be tested</td>
      </tr>
  </tbody>
</table>
<p>Of the three dedicated Coders, two failed catastrophically (full timeout and hardcoded mock string) and one didn&rsquo;t even run properly because of an infra bug. Meanwhile, the Qwen 3.5 35B-A3B, which is the general model in the line (not the Coder), came closest to something usable: 5 minutes of execution, recognizable Rails project, and the problem is fixable in 1-2 prompts.</p>
<p>Qwen 3 Coder 30B is particularly disappointing. It went so far past trying to use the API that it didn&rsquo;t really try at all: the controller it generated has this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Api</span><span class="o">::</span><span class="no">V1</span><span class="o">::</span><span class="no">MessagesController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">create</span>
</span></span><span class="line"><span class="cl">    <span class="n">render</span> <span class="ss">json</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="ss">response</span><span class="p">:</span> <span class="s2">&#34;This is a mock response. In a real implementation, this would connect to RubyLLM with Claude Sonnet via OpenRouter.&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The Gemfile lists <code>gem &quot;ruby_llm&quot;</code> but nothing imports it. The service layer is nonexistent. The model decided it was easier to return a fake string and call it a day. That&rsquo;s Tier 3 garbage in a way no correction prompt fixes — you have to tell it to start over.</p>
<p>Qwen 2.5 Coder 32B is even worse: 90 minutes running, zero files. The 1.8 MB <code>opencode-output.ndjson</code> shows the model spinning without managing to write anything. It probably got stuck in a planning loop without ever calling the write tools. Total slot waste.</p>
<p>Why did the &ldquo;Coder&rdquo; Qwens do so badly? My read is that the coding-specific fine-tuning they got was trained on more isolated problems (Codeforces, Leetcode, short snippets), far from agentic flows with long-running tool calling. The general Qwen 3.5 35B-A3B has broader training and handles the orchestration part better. The popular intuition &ldquo;Coder = best for coding agent&rdquo; is wrong for this kind of task. The use case where Coders shine is &ldquo;complete an isolated function,&rdquo; which is exactly what they were trained for, and that&rsquo;s a tiny fraction of what a coding agent does day to day.</p>
<h3>The question I wanted to answer<span class="hx:absolute hx:-mt-20" id="the-question-i-wanted-to-answer"></span>
    <a href="#the-question-i-wanted-to-answer" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>It was this: running locally on the 5090, which Qwen model is worth the 1-2 correction prompts to deliver code that works?</p>
<p>The honest answer is: only Qwen 3.5 35B-A3B, and maybe the Claude-distilled if you don&rsquo;t mind spending 12 minutes more.</p>
<ul>
<li>Qwen 3.5 35B-A3B on the 5090: 5 minutes, correct entry point (<code>RubyLLM.chat(model:)</code>), errors on the subsequent calls. Realistic total until it works: in the 15-20 minute range with 1-2 follow-ups. Beats cloud OSS on cost.</li>
<li>Qwen 3.5 27B Claude-distilled on the 5090: 12 minutes, deeper hallucination (entry point is invented too). Realistic total: 25-30 minutes with 2-3 follow-ups. Still competes on cost, and loses on absolute time to the real Claude.</li>
<li>The others (Coder 30B, Coder 2.5 32B, 3 32B): don&rsquo;t pay back the correction time. Each one has a structural problem that calls for a full rewrite from scratch.</li>
</ul>
<p>For folks with hardware in this category who want to escape Anthropic vendor lock-in, it now works. It didn&rsquo;t work on the 5090 from last year, and forget about it on the Strix Halo. In 2026, on NVIDIA Blackwell, with the right model, it works. For folks with low-bandwidth hardware (LPDDR5x, DDR4, DDR5), it&rsquo;s still a waste of time: the clock alone takes down any plan to make this practical.</p>
<h3>Qwen 3.6: what changed from 3.5<span class="hx:absolute hx:-mt-20" id="qwen-36-what-changed-from-35"></span>
    <a href="#qwen-36-what-changed-from-35" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>We tested two flavors of Qwen 3.6: the <strong>3.6 Plus</strong> (cloud, OpenRouter, free) and the <strong>3.6 35B</strong> (local, NVIDIA 5090, Q3_K_M).</p>
<p>Qwen 3.6 Plus (cloud) completed in 17 minutes with 88,940 tokens and 9/9 on the artifact checklist. Completed fast and for free. But the generated service uses <code>chat.add_message()</code>, a method that doesn&rsquo;t exist in RubyLLM. First message works, second one crashes. Same problem as 3.5.</p>
<p>Qwen 3.6 35B (local, 5090) is more interesting. Completed in 4.7 minutes at 240 tok/s, 169 files, correct entry point <code>RubyLLM.chat(model:, provider:)</code> and correct <code>chat.ask(message)</code>. The bug is subtler: it returns <code>response</code> instead of <code>response.content</code> and doesn&rsquo;t do history replay. One-line fix. That&rsquo;s a real improvement over Qwen 3.5 35B-A3B (which hallucinated <code>add_message</code> and <code>complete</code>). It&rsquo;s the cleanest Qwen result we&rsquo;ve seen.</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th>Version</th>
          <th>Hardware</th>
          <th style="text-align: center">Correct API?</th>
          <th style="text-align: center">Multi-turn works?</th>
          <th style="text-align: right">Time</th>
          <th style="text-align: right">Tok/s</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Qwen 3.5 35B-A3B</td>
          <td>3.5</td>
          <td>NVIDIA 5090</td>
          <td style="text-align: center">Entry point yes, <code>add_message</code>/<code>complete</code> no</td>
          <td style="text-align: center">No</td>
          <td style="text-align: right">5m</td>
          <td style="text-align: right">273</td>
      </tr>
      <tr>
          <td>Qwen 3.5 27B Claude-distilled</td>
          <td>3.5</td>
          <td>NVIDIA 5090</td>
          <td style="text-align: center">No (entry point invented)</td>
          <td style="text-align: center">No</td>
          <td style="text-align: right">12m</td>
          <td style="text-align: right">129</td>
      </tr>
      <tr>
          <td>Qwen 3.6 35B</td>
          <td>3.6</td>
          <td>NVIDIA 5090</td>
          <td style="text-align: center">Entry point yes, <code>chat.ask</code> yes, missing <code>.content</code></td>
          <td style="text-align: center">No (no replay)</td>
          <td style="text-align: right">5m</td>
          <td style="text-align: right">240</td>
      </tr>
      <tr>
          <td>Qwen 3.6 Plus</td>
          <td>3.6</td>
          <td>Cloud</td>
          <td style="text-align: center">Entry point yes, <code>add_message</code> no</td>
          <td style="text-align: center">No</td>
          <td style="text-align: right">17m</td>
          <td style="text-align: right">183</td>
      </tr>
  </tbody>
</table>
<p>The gap shrank but didn&rsquo;t close. The 3.6 35B local is closer to working than any previous Qwen — the bug is a forgotten <code>.content</code>, not an entirely invented API. But it still doesn&rsquo;t do multi-turn out of the box. In practice, Qwen 3.6 35B local moves up from Tier 3 to Tier 2: it&rsquo;s the local open source model that comes closest to delivering correct code on the first try, one fix away from working.</p>
<h2>The Deep Code Review: Sonnet vs GLM 5 vs Gemini vs Kimi vs MiniMax<span class="hx:absolute hx:-mt-20" id="the-deep-code-review-sonnet-vs-glm-5-vs-gemini-vs-kimi-vs-minimax"></span>
    <a href="#the-deep-code-review-sonnet-vs-glm-5-vs-gemini-vs-kimi-vs-minimax" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The tables above measure structural completeness. But does the project work? I did detailed code review of the models that completed the benchmark.</p>
<p><strong>Claude Sonnet 4.6 — works and is the most complete.</strong> Synchronous responses via Turbo Stream. Chat history persisted in a session cookie with full replay of previous messages on every request. Correct LLM mocking in the tests with mocha (30 tests in 328 lines). LLM logic extracted into a separate <code>LlmChatService</code>. Views decomposed into 9 partials. Minor problems: duplicated model constant, leak in the auto-resize event listener. None are blockers. Of the generated projects, it&rsquo;s the closest to something you&rsquo;d actually put into production.</p>
<p><strong>GLM 5 — works, but it&rsquo;s the bare minimum.</strong> Uses the correct API (<code>RubyLLM.chat(model:)</code> then <code>.ask()</code>), does mocking with mocha in the tests. But the project is way leaner than Sonnet&rsquo;s: 21-line controller (vs Sonnet&rsquo;s 52), no service layer (LLM logic inline in the controller), no chat history persistence, every message handled in isolation. The first message works, but the app doesn&rsquo;t keep conversation context, so you can&rsquo;t have a multi-turn dialog. The tests exist (7 methods) but they&rsquo;re skeletal: <code>ruby_llm_test.rb</code> only checks that the module is loaded, <code>chat_flow_test.rb</code> is a copy of the controller test. The Dockerfile, on the other hand, is the best of the four: multi-stage, non-root, jemalloc. But as a chat app? It&rsquo;s more of a proof of concept than something functional. Funny detail: the README says &ldquo;Powered by Claude Sonnet 4&rdquo; instead of the model that actually generated the project.</p>
<p><strong>Gemini 3.1 Pro — the fastest, but trips on the API.</strong> Completed in 14 minutes, the fastest along with MiniMax. The Rails code itself is well written: uses <code>Rails.cache</code> with session ID and a 2-hour expiration to keep state (instead of a database), Turbo Streams nicely integrated, Stimulus controller for auto-scroll, and the Dockerfile is the best of the group (multi-stage, non-root, jemalloc). The problem is the usual one: it uses <code>RubyLLM::Chat.new()</code> instead of <code>RubyLLM.chat()</code>, and calls <code>add_message()</code> which doesn&rsquo;t exist. The app boots, Docker runs, the health check passes, but the first chat message returns 500. The tests (5 methods) mock with a <code>FakeChat</code> that replicates the wrong signature, so they pass. It&rsquo;s frustrating because the rest of the code is the most &ldquo;Rails way&rdquo; of the non-Anthropic models. Fixing it would be 3 lines, but the benchmark measures what comes out the first time.</p>
<p><strong>Kimi K2.5 — ambitious but broken.</strong> Tried the most sophisticated architecture: ActionCable streaming, configurable models, dual Dockerfiles, 37 tests in 374 lines. Problem: the streaming depends on ActionCable, which is commented out in <code>config/application.rb</code>. The <code>return unless defined?(ActionCable)</code> guard makes the method do nothing. The assistant never responds. The Stimulus controller has a scope bug: <code>submitTarget</code> references a button outside the controller&rsquo;s subtree. Thread-unsafe storage with a hash in a class variable. Kimi wrote more tests than any other model (37), but none of them mock the LLM calls — so the tests pass without proving any of the functionality works.</p>
<p><strong>Grok 4.20 — fast and wrong.</strong> It was the fastest in the entire benchmark: 8 minutes, 412 tok/s. Except it was fast because it cut corners. The prompt explicitly asked for the <code>ruby_llm</code> gem, and Grok ignored it. It went straight for <code>OpenAI::Client</code> from the <code>ruby-openai</code> gem pointing at the OpenRouter URL. Technically the first message comes back, so yeah, it &ldquo;works.&rdquo; But it&rsquo;s the same trick as Step 3.5 Flash and Qwen 3.5 122B: skip the part that was actually being tested. No history, 33-line controller calling the HTTP client by hand, two tests, no real mocks. It was fast because it did less than what was asked.</p>
<p><strong>MiniMax M2.7 — looks right, crashes.</strong> Calls <code>RubyLLM.chat(model: '...', messages: [...])</code> — that signature doesn&rsquo;t exist. No message persistence. Duplicated HTML (DOCTYPE inside the layout). Committed master.key. And the tests? They mock the wrong API, so they pass but they don&rsquo;t prove anything.</p>
<p>Code review summary:</p>
<table>
  <thead>
      <tr>
          <th>Aspect</th>
          <th style="text-align: center">Sonnet 4.6</th>
          <th style="text-align: center">GLM 5</th>
          <th style="text-align: center">Gemini 3.1 Pro</th>
          <th style="text-align: center">Kimi K2.5</th>
          <th style="text-align: center">MiniMax M2.7</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Correct API</td>
          <td style="text-align: center">Yes</td>
          <td style="text-align: center">Yes</td>
          <td style="text-align: center">No</td>
          <td style="text-align: center">No</td>
          <td style="text-align: center">No</td>
      </tr>
      <tr>
          <td>Chat history</td>
          <td style="text-align: center">Session cookie</td>
          <td style="text-align: center">None</td>
          <td style="text-align: center">Rails.cache (2h)</td>
          <td style="text-align: center">Broken (ActionCable off)</td>
          <td style="text-align: center">None</td>
      </tr>
      <tr>
          <td>Service layer</td>
          <td style="text-align: center">LlmChatService</td>
          <td style="text-align: center">Inline in controller</td>
          <td style="text-align: center">LlmService</td>
          <td style="text-align: center">LlmService</td>
          <td style="text-align: center">ChatService (wrong API)</td>
      </tr>
      <tr>
          <td>Tests (methods)</td>
          <td style="text-align: center">30</td>
          <td style="text-align: center">7</td>
          <td style="text-align: center">5</td>
          <td style="text-align: center">37</td>
          <td style="text-align: center">12</td>
      </tr>
      <tr>
          <td>LLM mocking</td>
          <td style="text-align: center">Yes (mocha)</td>
          <td style="text-align: center">Yes (mocha)</td>
          <td style="text-align: center">FakeChat (wrong API)</td>
          <td style="text-align: center">No</td>
          <td style="text-align: center">Mocks wrong API</td>
      </tr>
      <tr>
          <td>Dockerfile</td>
          <td style="text-align: center">Multi-stage</td>
          <td style="text-align: center">Multi-stage + jemalloc</td>
          <td style="text-align: center">Multi-stage + jemalloc</td>
          <td style="text-align: center">Dual (dev/prod)</td>
          <td style="text-align: center">Single-stage</td>
      </tr>
      <tr>
          <td>Actually runs?</td>
          <td style="text-align: center">Yes</td>
          <td style="text-align: center">Yes (no history)</td>
          <td style="text-align: center">No (500 in chat)</td>
          <td style="text-align: center">No</td>
          <td style="text-align: center">No</td>
      </tr>
  </tbody>
</table>
<h3>GLM 5 vs GLM 5.1: what changed<span class="hx:absolute hx:-mt-20" id="glm-5-vs-glm-51-what-changed"></span>
    <a href="#glm-5-vs-glm-51-what-changed" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>GLM 5 was one of the few models that spat out functional code on the first try, so it was obvious to test the new version. One important detail before the numbers: GLM 5 ran via OpenRouter, GLM 5.1 wasn&rsquo;t there yet when I ran this test, so I used the Z.AI direct API. Different provider, different infra, different cache. The numbers below are reference, not exact measurement.</p>
<table>
  <thead>
      <tr>
          <th>Aspect</th>
          <th>GLM 5</th>
          <th>GLM 5.1</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Provider</td>
          <td>OpenRouter</td>
          <td>Z.AI direct</td>
      </tr>
      <tr>
          <td>Total time</td>
          <td>17m</td>
          <td>22m</td>
      </tr>
      <tr>
          <td>Tok/s (final phase)</td>
          <td>400</td>
          <td>167</td>
      </tr>
      <tr>
          <td>Effective new tokens</td>
          <td>1,138</td>
          <td>450</td>
      </tr>
      <tr>
          <td>Cache read</td>
          <td>58,240</td>
          <td>81,216</td>
      </tr>
      <tr>
          <td>Correct RubyLLM API</td>
          <td>Yes</td>
          <td>Yes</td>
      </tr>
      <tr>
          <td>Test mocking</td>
          <td>Yes (mocha)</td>
          <td>Yes</td>
      </tr>
      <tr>
          <td>Tests</td>
          <td>7</td>
          <td>24</td>
      </tr>
      <tr>
          <td>Chat history</td>
          <td>No</td>
          <td>Yes (in-memory)</td>
      </tr>
      <tr>
          <td>Service layer</td>
          <td>Inline in controller</td>
          <td><code>ChatSession</code> model with <code>add_user_message</code>/<code>add_assistant_message</code></td>
      </tr>
  </tbody>
</table>
<p>The GLM 5.1 project came out way more complete. 24 tests vs 7. Real separation between <code>ChatSession</code>, <code>ChatMessage</code> and the controller, instead of GLM 5 cramming everything inline. Chat history persisted in memory during the session, so you can actually have a real multi-turn conversation (GLM 5 treated every message like it was the first). And the RubyLLM API is still correct, the same <code>RubyLLM.chat(model:, provider:)</code> pattern followed by <code>c.user</code>/<code>c.assistant</code> to build the context. There&rsquo;s even a test covering the <code>MODEL</code> constant, which usually nobody does.</p>
<p>The price was speed. 22 minutes vs 17, and throughput dropped from 400 to 167 tok/s. Could be the provider (Z.AI direct isn&rsquo;t the same infra as OpenRouter), could be a more loaded server during the run, could be that 5.1 reasons more. I didn&rsquo;t run it multiple times to take an average, so I won&rsquo;t say 5.1 is &ldquo;slower.&rdquo; A single run doesn&rsquo;t prove a regression. What I can say is that, in my test, 5.1 delivered a better-structured project and took a bit longer to do it.</p>
<p>For folks who want to get out from under Anthropic without losing quality, GLM 5 and GLM 5.1 are the two options that work. If you need centralized billing on OpenRouter, GLM 5. If you can use Z.AI direct and want a more rounded project on the first try, GLM 5.1.</p>
<h2>Costs: API vs Subscription<span class="hx:absolute hx:-mt-20" id="costs-api-vs-subscription"></span>
    <a href="#costs-api-vs-subscription" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>First, the per-token price of each model on OpenRouter:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/05/llm-benchmark/en/token-pricing.png" alt="Per-token price on OpenRouter"  loading="lazy" /></p>
<p>GPT 5.4 Pro charges $180 per million output tokens. Claude Opus charges $25. GLM 5 charges $2.30. And Qwen 3.6 Plus is free (with a rate limit). The log scale on the chart hides some of the brutality of the gap: from free Qwen to GPT 5.4 Pro is orders of magnitude.</p>
<p>But per-token price isn&rsquo;t the whole story. If you use Claude or GPT daily for coding, the monthly subscription can come out way cheaper than paying per token via the API:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/05/llm-benchmark/en/monthly-pricing.png" alt="Subscription vs API: how much it costs to use Claude and GPT per month"  loading="lazy" /></p>
<table>
  <thead>
      <tr>
          <th>Approach</th>
          <th style="text-align: right">Est. $/month*</th>
          <th>Notes</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Qwen 3.6 Plus (OpenRouter)</td>
          <td style="text-align: right">$0</td>
          <td>Free but rate-limited</td>
      </tr>
      <tr>
          <td>Local models</td>
          <td style="text-align: right">Electricity</td>
          <td>Needs hardware</td>
      </tr>
      <tr>
          <td>Claude Pro</td>
          <td style="text-align: right">$20</td>
          <td>~44K tokens/5hr</td>
      </tr>
      <tr>
          <td>ChatGPT Plus</td>
          <td style="text-align: right">$20</td>
          <td>Includes Codex</td>
      </tr>
      <tr>
          <td>Claude Max 5x</td>
          <td style="text-align: right">$100</td>
          <td>~88K tokens/5hr</td>
      </tr>
      <tr>
          <td>Claude Sonnet (OpenRouter API)</td>
          <td style="text-align: right">~$150</td>
          <td>No cap, pay-as-you-go</td>
      </tr>
      <tr>
          <td>Claude Max 20x</td>
          <td style="text-align: right">$200</td>
          <td>~220K tokens/5hr</td>
      </tr>
      <tr>
          <td>ChatGPT Pro</td>
          <td style="text-align: right">$200</td>
          <td>GPT 5.4 Pro unlimited</td>
      </tr>
      <tr>
          <td>Claude Opus (OpenRouter API)</td>
          <td style="text-align: right">~$450</td>
          <td>No cap, pay-as-you-go</td>
      </tr>
      <tr>
          <td>GPT 5.4 Pro (OpenRouter API)</td>
          <td style="text-align: right">~$990</td>
          <td>Absurdly expensive</td>
      </tr>
  </tbody>
</table>
<p>*Estimate for moderate coding use (~15M input + ~3M output tokens/month).</p>
<p>The main point: if you use GPT 5.4 Pro, the ChatGPT Pro subscription at $200/month with unlimited use is 5x cheaper than paying per token on the API. For Claude, Pro at $20/month covers light use, but for heavy users (a coding marathon like mine), the Max 20x at $200/month comes out cheaper than paying for Opus per token on OpenRouter (~$450/month). The open source models on OpenRouter all sit below $2.50/M output tokens, but as we saw, most of them generate code that doesn&rsquo;t run.</p>
<h2>What works for real use<span class="hx:absolute hx:-mt-20" id="what-works-for-real-use"></span>
    <a href="#what-works-for-real-use" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>After testing 33 models across both runs and looking at the generated code in detail:</p>
<p>Tier 1 (works plug and play):</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th>Quality</th>
          <th style="text-align: right">Cost/Run</th>
          <th>Trade-off</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Claude Opus 4.7</td>
          <td>New baseline (28 tests, FakeChat, 96.7% coverage)</td>
          <td style="text-align: right">~$1.10</td>
          <td>Incremental over 4.6, new gold standard</td>
      </tr>
      <tr>
          <td>Claude Sonnet 4.6</td>
          <td>Better than Opus 4.6 on opencode (30 vs 16 tests)</td>
          <td style="text-align: right">~$0.63</td>
          <td>Cheaper, but fails on deeper reasoning in real projects</td>
      </tr>
      <tr>
          <td>Claude Opus 4.6</td>
          <td>Previous gold standard</td>
          <td style="text-align: right">~$1.05</td>
          <td>Previous baseline</td>
      </tr>
      <tr>
          <td>GLM 5</td>
          <td>Good (7 tests, correct API)</td>
          <td style="text-align: right">~$0.11</td>
          <td>89% cheaper, non-Anthropic/OpenAI alternative that works</td>
      </tr>
      <tr>
          <td>GLM 5.1</td>
          <td>Good (24 tests, history, correct API)</td>
          <td style="text-align: right">~$0.13</td>
          <td>~88% cheaper, more complete project than GLM 5</td>
      </tr>
  </tbody>
</table>
<p>Tier 2 (works with caveats):</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th style="text-align: center">Hardware</th>
          <th style="text-align: right">Cost/Run</th>
          <th>Caveat</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>GPT 5.4 (Codex)</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: right">~$16.00</td>
          <td>Impressive architecture (22 tests, dependency injection, PORO models), but <code>add_message</code> with keyword args instead of positional hash breaks multi-turn. 7.6M tokens, 15x more expensive than Opus</td>
      </tr>
      <tr>
          <td>Step 3.5 Flash</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: right">~$0.02</td>
          <td>Bypasses the requested gem, slow (38m)</td>
      </tr>
      <tr>
          <td>Grok 4.20</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: right">~$0.04</td>
          <td>Bypasses the requested gem (goes straight to <code>OpenAI::Client</code>), but it&rsquo;s the fastest in the benchmark</td>
      </tr>
      <tr>
          <td>Qwen 3.6 35B</td>
          <td style="text-align: center">NVIDIA 5090</td>
          <td style="text-align: right">Free</td>
          <td>Entry point and <code>chat.ask</code> correct, missing <code>.content</code>. One-line fix. ~10-15 min total</td>
      </tr>
      <tr>
          <td>Qwen 3.5 35B-A3B</td>
          <td style="text-align: center">NVIDIA 5090</td>
          <td style="text-align: right">Free</td>
          <td>Correct entry point, hallucinates <code>add_message</code>/<code>complete</code>. Fixable in 1-2 follow-ups. ~15-20 min total</td>
      </tr>
      <tr>
          <td>Qwen 3.5 27B Claude-distilled</td>
          <td style="text-align: center">NVIDIA 5090</td>
          <td style="text-align: right">Free</td>
          <td>Claude style, complete API hallucination. 2-3 follow-ups to fix. ~25-30 min total</td>
      </tr>
      <tr>
          <td>Qwen 3.5 35B (local)</td>
          <td style="text-align: center">AMD Strix</td>
          <td style="text-align: right">Free</td>
          <td>Works if default is configured, no mocking, and slow</td>
      </tr>
  </tbody>
</table>
<p>Tier 3 (broken code, easier to redo than to fix):</p>
<p>Kimi K2.5, MiniMax M2.7, DeepSeek V3.2, Gemini 3.1 Pro, Qwen 3 Coder Next (Strix), Qwen 3 Coder 30B (5090, returned a hardcoded mock string), Qwen 3.5 122B, Qwen 3.6 Plus — all of them either invent APIs that don&rsquo;t exist or don&rsquo;t even try to use the gem.</p>
<p>Tier 4 (didn&rsquo;t complete):</p>
<p>Gemma 4 (infinite loop on both hardware), Llama 4 Scout (no parser), GPT OSS 20B (wrong directory on Strix, parser regression on 5090), Qwen 3 32B (too slow on Strix, partial scaffold on 5090), Qwen 2.5 Coder 32B (90m timeout with zero files).</p>
<h3>Simplified ranking (quality, time, price)<span class="hx:absolute hx:-mt-20" id="simplified-ranking-quality-time-price"></span>
    <a href="#simplified-ranking-quality-time-price" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>For folks who only want the report-card summary. Quality is whether the code runs and how complete it is. Time is the total runtime. Price is the estimated cost per execution on opencode. <strong>Hardware</strong> indicates where the model ran — Cloud, Strix (AMD Strix Halo, LPDDR5x 256 GB/s) or 5090 (NVIDIA RTX 5090, GDDR7 1792 GB/s). Cloud models ran via OpenRouter or the provider&rsquo;s direct API.</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th style="text-align: center">Type</th>
          <th style="text-align: center">Hardware</th>
          <th style="text-align: center">Quality</th>
          <th style="text-align: center">Time</th>
          <th style="text-align: center">Price</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Claude Opus 4.7</td>
          <td style="text-align: center">Commercial</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: center">A+</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">D</td>
      </tr>
      <tr>
          <td>Claude Sonnet 4.6</td>
          <td style="text-align: center">Commercial</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: center">A+</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">C</td>
      </tr>
      <tr>
          <td>Claude Opus 4.6</td>
          <td style="text-align: center">Commercial</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: center">A+</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">D</td>
      </tr>
      <tr>
          <td>GPT 5.4 (Codex)</td>
          <td style="text-align: center">Commercial</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: center">B+</td>
          <td style="text-align: center">B</td>
          <td style="text-align: center">F</td>
      </tr>
      <tr>
          <td>GLM 5.1</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">B</td>
          <td style="text-align: center">A</td>
      </tr>
      <tr>
          <td>GLM 5</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: center">A−</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">A</td>
      </tr>
      <tr>
          <td>Qwen 3.6 35B</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">5090</td>
          <td style="text-align: center">B+</td>
          <td style="text-align: center">A+</td>
          <td style="text-align: center">A+ (free)</td>
      </tr>
      <tr>
          <td>Qwen 3.5 35B-A3B</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">5090</td>
          <td style="text-align: center">B</td>
          <td style="text-align: center">A+</td>
          <td style="text-align: center">A+ (free)</td>
      </tr>
      <tr>
          <td>Qwen 3.5 27B Claude-distilled</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">5090</td>
          <td style="text-align: center">C+</td>
          <td style="text-align: center">B</td>
          <td style="text-align: center">A+ (free)</td>
      </tr>
      <tr>
          <td>Gemini 3.1 Pro</td>
          <td style="text-align: center">Commercial</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: center">C</td>
          <td style="text-align: center">A+</td>
          <td style="text-align: center">B</td>
      </tr>
      <tr>
          <td>Grok 4.20</td>
          <td style="text-align: center">Commercial</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: center">C−</td>
          <td style="text-align: center">A+</td>
          <td style="text-align: center">A+</td>
      </tr>
      <tr>
          <td>Step 3.5 Flash</td>
          <td style="text-align: center">Commercial</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: center">C−</td>
          <td style="text-align: center">D</td>
          <td style="text-align: center">A+</td>
      </tr>
      <tr>
          <td>Qwen 3.5 35B-A3B</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">Strix</td>
          <td style="text-align: center">C</td>
          <td style="text-align: center">C</td>
          <td style="text-align: center">A+ (free)</td>
      </tr>
      <tr>
          <td>Qwen 3 Coder Next</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">Strix</td>
          <td style="text-align: center">D+</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">A+ (free)</td>
      </tr>
      <tr>
          <td>Qwen 3 32B</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">5090</td>
          <td style="text-align: center">D</td>
          <td style="text-align: center">A+</td>
          <td style="text-align: center">A+ (free)</td>
      </tr>
      <tr>
          <td>Qwen 3 Coder 30B</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">5090</td>
          <td style="text-align: center">D−</td>
          <td style="text-align: center">A+</td>
          <td style="text-align: center">A+ (free)</td>
      </tr>
      <tr>
          <td>Qwen 3.6 Plus</td>
          <td style="text-align: center">Commercial</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: center">D</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">A+ (free)</td>
      </tr>
      <tr>
          <td>Kimi K2.5</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: center">D</td>
          <td style="text-align: center">C</td>
          <td style="text-align: center">A</td>
      </tr>
      <tr>
          <td>MiniMax M2.7</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: center">D</td>
          <td style="text-align: center">A</td>
          <td style="text-align: center">A+</td>
      </tr>
      <tr>
          <td>Qwen 3.5 122B</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">Strix</td>
          <td style="text-align: center">D</td>
          <td style="text-align: center">F</td>
          <td style="text-align: center">A+ (free)</td>
      </tr>
      <tr>
          <td>DeepSeek V3.2</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">Cloud</td>
          <td style="text-align: center">F</td>
          <td style="text-align: center">F</td>
          <td style="text-align: center">A+</td>
      </tr>
      <tr>
          <td>Qwen 2.5 Coder 32B</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">5090</td>
          <td style="text-align: center">F</td>
          <td style="text-align: center">F</td>
          <td style="text-align: center">A+ (free)</td>
      </tr>
      <tr>
          <td>Gemma 4 31B</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">5090</td>
          <td style="text-align: center">F</td>
          <td style="text-align: center">A+</td>
          <td style="text-align: center">A+ (free)</td>
      </tr>
      <tr>
          <td>Gemma 4 31B</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">Strix</td>
          <td style="text-align: center">F</td>
          <td style="text-align: center">—</td>
          <td style="text-align: center">A+ (free)</td>
      </tr>
      <tr>
          <td>GLM 4.7 Flash</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">Strix</td>
          <td style="text-align: center">F</td>
          <td style="text-align: center">—</td>
          <td style="text-align: center">A+ (free)</td>
      </tr>
      <tr>
          <td>Llama 4 Scout</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">Strix</td>
          <td style="text-align: center">F</td>
          <td style="text-align: center">—</td>
          <td style="text-align: center">A+ (free)</td>
      </tr>
      <tr>
          <td>GPT OSS 20B</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">Strix</td>
          <td style="text-align: center">F</td>
          <td style="text-align: center">—</td>
          <td style="text-align: center">A+ (free)</td>
      </tr>
      <tr>
          <td>Qwen 3 32B</td>
          <td style="text-align: center">OSS</td>
          <td style="text-align: center">Strix</td>
          <td style="text-align: center">F</td>
          <td style="text-align: center">—</td>
          <td style="text-align: center">A+ (free)</td>
      </tr>
  </tbody>
</table>
<p>Quality criteria: A+ works and the code is well structured. A/B works with small to medium caveats. C runs but skips a prompt requirement or has a serious structural issue. D breaks on the first message because of an invented API. F didn&rsquo;t complete the benchmark or produced garbage. GPT 5.4 via Codex dropped from A+ to B+: the architecture is the most sophisticated in the benchmark (dependency injection, PORO models, 22 tests), but the first message works and multi-turn breaks due to a wrong calling convention on <code>add_message</code>. Burned 7.6M tokens (~$16/run) without reaching Opus-level correctness. &ldquo;Type&rdquo; separates commercial models (closed weights) from OSS (open weights, even when used through a hosted API). Some Qwens appear twice when they ran on both hardware profiles, because the results are different enough to justify it — Qwen 3.5 35B-A3B on the 5090 jumps to Tier B, on the Strix it stays at Tier C because of the wait time. Of the 33 models configured across both runs, some don&rsquo;t appear in this table because they never even executed (no quota, broken runner, infra failure, or timeout before the first message).</p>
<h3>The verdict<span class="hx:absolute hx:-mt-20" id="the-verdict"></span>
    <a href="#the-verdict" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>If you want the best result and don&rsquo;t want to think about it: <strong>Claude Opus</strong>. Opus 4.7 is the new baseline — 28 tests, correct API, 96.7% coverage, FakeChat pattern in the tests. It&rsquo;s an incremental improvement over 4.6 (which had 16 tests), not a revolution. But it doesn&rsquo;t need to be a revolution. 4.6 already worked, 4.7 works the same and delivers a slightly more polished project. If you were on 4.6, upgrading to 4.7 means switching to a model that does the same thing with more care in the tests and structure.</p>
<p>A word about Sonnet: it beat Opus in this benchmark (30 tests vs 16 for Opus 4.6, vs 28 for Opus 4.7). But this benchmark is a small, well-defined web app. In my real-world experience with bigger projects, Sonnet fails when the reasoning needs to go deeper. I&rsquo;m not talking about massive projects — just going a bit further than this benchmark (more controllers, more integrations, architectural decisions that depend on each other) is enough for Sonnet to lose the thread. Opus has a 128K max output token ceiling vs Sonnet&rsquo;s 64K, and its training was specifically aimed at long-horizon tasks, multi-step planning and deep reasoning over complex code. On a small project like the benchmark, those muscles stay idle, and in that scenario Sonnet wins by being faster and cheaper. But if you extrapolate that to &ldquo;Sonnet is better than Opus,&rdquo; you&rsquo;ll get a surprise on the first task that requires sustained reasoning. You can try Sonnet — it&rsquo;s cheaper and for small projects it works. But for real projects, you&rsquo;ll probably end up on Opus anyway.</p>
<p>On GPT 5.4: we now have objective data. Ran it via Codex CLI with xHigh reasoning effort. The architecture it generates is the most sophisticated in the benchmark — dependency injection, PORO models, session management with TTL. But it burned 7.6M tokens (~$16/run, 15x more expensive than Opus) and got the <code>add_message</code> calling convention wrong (keyword args instead of positional hash), breaking multi-turn. Spent more, got it wrong. Dropped from Tier 1 to Tier 2. Same pattern: API correctness is binary recall in the weights. It doesn&rsquo;t scale with token budget or reasoning effort.</p>
<p>If cost matters and you want to leave Anthropic: <strong>GLM 5 or GLM 5.1</strong> are the plug-and-play alternatives that work. Correct API, mocking in the tests, ~$0.11-$0.13 per run, ~88-89% cheaper than Opus. GLM 5.1 delivered a more complete project (24 tests, chat history) at the cost of about 5 more minutes.</p>
<p>If you want to avoid total vendor lock-in and you have decent hardware: <strong>Qwen 3.6 35B</strong> running locally on an NVIDIA RTX 5090. Under 5 minutes of execution at 240 tok/s, correct entry point and <code>chat.ask</code>, just missing <code>.content</code> extraction — a 1-line fix. That&rsquo;s better than the Qwen 3.5 35B-A3B which hallucinated entire API methods. The 3.6 generation is the first Qwen that&rsquo;s genuinely one fix away from working, not a rewrite away. Realistic total: ~10-15 minutes with one follow-up.</p>
<p>If you want to avoid vendor lock-in but you&rsquo;re on weak hardware: <strong>GLM 5 or GLM 5.1</strong> remain the choice. They&rsquo;re cloud, true, but at $0.11-$0.13 per run it&rsquo;s basically the price of electricity.</p>
<p>If you want to test the &ldquo;Claude at home&rdquo; gamble via distillation: the <strong>Qwen 3.5 27B Claude-distilled</strong> is sitting there to play with, but I already warned you it hallucinates exactly the same fake APIs as the base Qwen. Distillation transferred Claude&rsquo;s style, not its factual knowledge about libraries. It&rsquo;s worth it as an experiment, not as production.</p>
<p>Yes, maybe with days of tweaking llama.cpp, calibrating flags, adjusting prompts, testing different builds, you could make Gemma 4 or other models work better. For most people, that isn&rsquo;t realistic. The distance between frontier models (Claude, GPT) and self-hosted open source models is real. It isn&rsquo;t marketing. The gap is shrinking, but it still exists, and the nature of it has changed: today what&rsquo;s missing in open source is factual knowledge about specific libraries, not raw reasoning capacity. Hardware stopped being the bottleneck, at least for anyone with a recent GPU.</p>
<p>In the end, what matters is whether the code runs. A model can generate 3,405 files, write 37 tests, produce a 181-line README, and the app still won&rsquo;t work because the API it uses doesn&rsquo;t exist. Completeness metrics and test counts are necessary but not sufficient. The only reliable signal is whether the model uses real APIs correctly.</p>
<p>The full benchmark, with code, configuration, prompts and per-model results, is on <a href="https://github.com/akitaonrails/llm-coding-benchmark"target="_blank" rel="noopener">GitHub</a>.</p>
]]></content:encoded><category>llm-benchmarks</category><category>llms</category><category>local-models</category></item><item><title>Review: Minisforum MS-S1 Max | AMD AI Max+ 395 with 96GB of VRAM</title><link>https://akitaonrails.github.io/en/2026/03/31/minisforum-ms-s1-max-amd-ai-max-395-review/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/03/31/minisforum-ms-s1-max-amd-ai-max-395-review/</guid><pubDate>Tue, 31 Mar 2026 15:00:00 -0300</pubDate><description>&lt;p&gt;If you&amp;rsquo;ve been following my &lt;a href="https://akitaonrails.github.io/en/2024/04/03/my-personal-netflix-with-docker-compose/"&gt;home server posts&lt;/a&gt;, you know I used to run everything on an Intel NUC Core i7 with 32GB of RAM. It worked. But as open source AI models grew, the NUC became the bottleneck. Without a dedicated GPU, any LLM inference would fall back to the CPU and turn unusable.&lt;/p&gt;
&lt;p&gt;I bought a Minisforum MS-S1 Max with the new AMD Ryzen AI Max+ 395 chip for one specific reason: this chip supports up to 128GB of unified RAM, and I can allocate 96GB of it as VRAM for the iGPU. That gives me more VRAM than any consumer gaming card, including the RTX 5090 (32GB). And that changes what I can run locally.&lt;/p&gt;</description><content:encoded><![CDATA[<p>If you&rsquo;ve been following my <a href="/en/2024/04/03/my-personal-netflix-with-docker-compose/">home server posts</a>, you know I used to run everything on an Intel NUC Core i7 with 32GB of RAM. It worked. But as open source AI models grew, the NUC became the bottleneck. Without a dedicated GPU, any LLM inference would fall back to the CPU and turn unusable.</p>
<p>I bought a Minisforum MS-S1 Max with the new AMD Ryzen AI Max+ 395 chip for one specific reason: this chip supports up to 128GB of unified RAM, and I can allocate 96GB of it as VRAM for the iGPU. That gives me more VRAM than any consumer gaming card, including the RTX 5090 (32GB). And that changes what I can run locally.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/blog/2026/03/31/minisforum-desk.jpg" alt="Minisforum MS-S1 Max on the desk"  loading="lazy" /></p>
<h2>Why ditch the Intel NUC<span class="hx:absolute hx:-mt-20" id="why-ditch-the-intel-nuc"></span>
    <a href="#why-ditch-the-intel-nuc" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The NUC was a fine Docker server for two years. But the limitation was clear: without a GPU with enough VRAM, I couldn&rsquo;t run LLMs locally in any usable way. <a href="https://github.com/akitaonrails/FrankYomik"target="_blank" rel="noopener">Frank Yomik</a>, my automatic manga translation system, needed CPU-based OCR (slow) and would connect remotely to the Ollama running on my desktop (AMD 7950X3D + RTX 5090) for translation. It worked, but it meant my desktop had to be on for the server to do its job.</p>
<p><img src="https://raw.githubusercontent.com/akitaonrails/FrankYomik/master/docs/sample_translate.png" alt="Frank Yomik - automatic manga translation"  loading="lazy" /></p>
<p>With the Minisforum, Frank Yomik now runs entirely on the server. The worker uses ROCm for OCR on the iGPU, and Ollama runs locally with 96GB of VRAM. Zero dependency on the desktop.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/blog/2026/03/31/minisforum-nuc-compare.jpg" alt="Comparison: Intel NUC (left) vs Minisforum MS-S1 Max (right)"  loading="lazy" /></p>
<p>You can get a sense of the size from the photo. The NUC is the tiny cube on the left. The Minisforum is bigger but it&rsquo;s still a mini-PC. It fits on the rack shelf under my Synology NAS without any trouble.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/blog/2026/03/31/minisforum-shelf.jpg" alt="Minisforum installed on the shelf, next to the NAS"  loading="lazy" /></p>
<h2>The specs<span class="hx:absolute hx:-mt-20" id="the-specs"></span>
    <a href="#the-specs" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/blog/2026/03/31/minisforum-fastfetch.png" alt="fastfetch on the Minisforum"  loading="lazy" /></p>
<p>The chip is the AMD Ryzen AI Max+ 395: 16 cores / 32 threads Zen 5, with an integrated Radeon 8060S iGPU and 128GB of unified LPDDR5X. In the BIOS, I set the UMA Frame Buffer Size to 96GB, which leaves ~30GB of RAM for the operating system and containers. Plus the kernel parameters for TTM (without them, ROCm only sees 15.5GB even with the BIOS allocation in place).</p>
<p>The operating system is openSUSE MicroOS (more on that in the <a href="/en/2026/03/31/migrating-my-home-server-with-claude-code/">next post</a>). The whole machine pulls under 100W, which is absurd if you&rsquo;re used to dedicated GPUs that draw 450W+ on their own.</p>
<h2>Minisforum vs my desktop: benchmarks<span class="hx:absolute hx:-mt-20" id="minisforum-vs-my-desktop-benchmarks"></span>
    <a href="#minisforum-vs-my-desktop-benchmarks" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I ran a <a href="https://github.com/akitaonrails/homelab-docs/tree/master/benchmarks"target="_blank" rel="noopener">set of benchmarks</a> comparing the Minisforum against my desktop (AMD 7950X3D, 96GB DDR5, RTX 5090 32GB GDDR7). The results are clear.</p>
<h3>CPU<span class="hx:absolute hx:-mt-20" id="cpu"></span>
    <a href="#cpu" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><table>
  <thead>
      <tr>
          <th>Test</th>
          <th>7950X3D</th>
          <th>AI Max+ 395</th>
          <th>Winner</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Prime sieve (single-core)</td>
          <td>0.021s</td>
          <td>0.018s</td>
          <td>Strix Halo +14%</td>
      </tr>
      <tr>
          <td>Float pi (single-core)</td>
          <td>1.335s</td>
          <td>1.706s</td>
          <td>7950X3D +28%</td>
      </tr>
      <tr>
          <td>Multi-core sieve (32 threads)</td>
          <td>0.181s</td>
          <td>0.118s</td>
          <td>Strix Halo +53%</td>
      </tr>
      <tr>
          <td>SHA-256 throughput</td>
          <td>2.714 MB/s</td>
          <td>2.488 MB/s</td>
          <td>7950X3D +9%</td>
      </tr>
      <tr>
          <td>AES-256-CBC throughput</td>
          <td>1.613 MB/s</td>
          <td>1.410 MB/s</td>
          <td>7950X3D +14%</td>
      </tr>
  </tbody>
</table>
<p>Mixed results. The AI Max+ 395 is better at pure parallelism (multi-core sieve), probably thanks to lower latency in the unified memory architecture. The 7950X3D wins at float and crypto because of its higher clocks and the 3D V-Cache.</p>
<h3>LLM inference (models that fit on both)<span class="hx:absolute hx:-mt-20" id="llm-inference-models-that-fit-on-both"></span>
    <a href="#llm-inference-models-that-fit-on-both" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>This is where it gets interesting. For models that fit in the 32GB of the RTX 5090, the comparison is purely about memory bandwidth:</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th>Size</th>
          <th>RTX 5090 (tok/s)</th>
          <th>Strix Halo (tok/s)</th>
          <th>5090 advantage</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>phi4</td>
          <td>9.1 GB</td>
          <td>155.1</td>
          <td>23.2</td>
          <td>6.7x</td>
      </tr>
      <tr>
          <td>qwen3:14b</td>
          <td>9.3 GB</td>
          <td>138.9</td>
          <td>22.6</td>
          <td>6.1x</td>
      </tr>
      <tr>
          <td>phi4-reasoning</td>
          <td>11.1 GB</td>
          <td>130.2</td>
          <td>19.1</td>
          <td>6.8x</td>
      </tr>
      <tr>
          <td>qwen3:32b</td>
          <td>20.2 GB</td>
          <td>66.9</td>
          <td>10.0</td>
          <td>6.7x</td>
      </tr>
  </tbody>
</table>
<p>The RTX 5090 is ~7x faster. The explanation is simple: GDDR7 has ~1,792 GB/s of bandwidth. LPDDR5X has ~256 GB/s. The ratio (7x) lines up almost exactly with the measured speed difference (6.7x). LLM inference is a problem dominated by memory bandwidth. Whoever reads weights faster, generates tokens faster.</p>
<h3>And what about prompt processing?<span class="hx:absolute hx:-mt-20" id="and-what-about-prompt-processing"></span>
    <a href="#and-what-about-prompt-processing" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><table>
  <thead>
      <tr>
          <th>Model</th>
          <th>RTX 5090 (tok/s)</th>
          <th>Strix Halo (tok/s)</th>
          <th>5090 advantage</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>phi4</td>
          <td>~1,933</td>
          <td>~212</td>
          <td>9.1x</td>
      </tr>
      <tr>
          <td>qwen3:14b</td>
          <td>~1,474</td>
          <td>~155</td>
          <td>9.5x</td>
      </tr>
      <tr>
          <td>qwen3:32b</td>
          <td>~767</td>
          <td>~68</td>
          <td>11.3x</td>
      </tr>
  </tbody>
</table>
<p>Prompt processing is even worse: 7-11x slower. That makes sense, because the prompt has to be processed in full before generating the first token, and it&rsquo;s an even more bandwidth-intensive operation.</p>
<h3>Where the Strix Halo wins: large models<span class="hx:absolute hx:-mt-20" id="where-the-strix-halo-wins-large-models"></span>
    <a href="#where-the-strix-halo-wins-large-models" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Now we get to the reason I bought this PC. Models that don&rsquo;t fit in the RTX 5090:</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th>Size</th>
          <th>Strix Halo (tok/s)</th>
          <th>Notes</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>gpt-oss:20b</td>
          <td>13.8 GB MXFP4</td>
          <td>48.9</td>
          <td>MoE, faster than expected</td>
      </tr>
      <tr>
          <td>qwen3.5:35b</td>
          <td>23.9 GB</td>
          <td>43.2</td>
          <td>MoE, only ~4B active params</td>
      </tr>
      <tr>
          <td>qwen3-coder-next</td>
          <td>51.7 GB</td>
          <td>29.5</td>
          <td>MoE, 50GB+</td>
      </tr>
      <tr>
          <td>qwen3.5:122b</td>
          <td>81.4 GB Q4_K_M</td>
          <td>19.2</td>
          <td>122B params, MoE</td>
      </tr>
      <tr>
          <td>glm-4.7-flash:bf16</td>
          <td>59.9 GB</td>
          <td>17.9</td>
          <td>Full precision bf16</td>
      </tr>
      <tr>
          <td>qwen2.5:72b</td>
          <td>47.4 GB Q4_K_M</td>
          <td>4.5</td>
          <td>Dense 72B, bandwidth-limited</td>
      </tr>
  </tbody>
</table>
<p>qwen3.5:122b with 81GB of weights running at 19 tok/s. On a mini-PC. That&rsquo;s simply not possible on an RTX 5090. On the NVIDIA card, that model would have to offload layers to system RAM, dropping to 2-3 tok/s. In practice, unusable.</p>
<p>The difference between MoE and dense models is brutal. qwen3.5:35b runs at 43 tok/s because, despite having 35B total parameters, only ~4B are active per token. A dense 72B model like qwen2.5:72b has to read 40GB+ of weights per token, and at 256 GB/s of bandwidth, the theoretical maximum is ~6.7 tok/s. The 4.5 measured represent ~67% efficiency, which is what you&rsquo;d expect for an iGPU (overhead from shared bus and drivers).</p>
<h3>Summary: when to use which machine<span class="hx:absolute hx:-mt-20" id="summary-when-to-use-which-machine"></span>
    <a href="#summary-when-to-use-which-machine" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><table>
  <thead>
      <tr>
          <th>Use case</th>
          <th>Best machine</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Interactive chat/coding (models &lt;32GB)</td>
          <td>RTX 5090 (6-7x faster)</td>
      </tr>
      <tr>
          <td>Large models (50GB+)</td>
          <td>Strix Halo (only option)</td>
      </tr>
      <tr>
          <td>Dense 70B+ models</td>
          <td>Strix Halo (only option)</td>
      </tr>
      <tr>
          <td>Full-precision bf16</td>
          <td>Strix Halo (only option)</td>
      </tr>
      <tr>
          <td>Batch processing with long context</td>
          <td>Strix Halo (more VRAM for KV cache)</td>
      </tr>
      <tr>
          <td>API serving with low latency</td>
          <td>RTX 5090 (sub-150ms TTFT)</td>
      </tr>
  </tbody>
</table>
<h3>A ROCm bug that&rsquo;s still around<span class="hx:absolute hx:-mt-20" id="a-rocm-bug-thats-still-around"></span>
    <a href="#a-rocm-bug-thats-still-around" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Not everything works. Models like deepseek-r1:70b, llama3.3:70b and llama4:scout crash with a ggml bug (<code>GGML_ASSERT(ggml_nbytes(src0) &lt;= INT_MAX) failed</code>). The embedding tensor of these models exceeds 2GB and the ROCm copy kernel uses a 32-bit integer for the size. On CUDA (NVIDIA) it&rsquo;s already been fixed, but on ROCm it hasn&rsquo;t. Waiting for the fix in Ollama 0.20.0+.</p>
<h2>LPDDR5X vs GDDR7: why this difference exists<span class="hx:absolute hx:-mt-20" id="lpddr5x-vs-gddr7-why-this-difference-exists"></span>
    <a href="#lpddr5x-vs-gddr7-why-this-difference-exists" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The next question is: why is LPDDR5X so much slower?</p>
<p>GDDR7 is dedicated GPU memory. It&rsquo;s soldered onto the graphics card, connected by a wide bus (384 or 512 bits on the RTX 5090) at high clocks. Its only job is to feed data to the GPU. LPDDR5X is unified memory that serves everything: the operating system, applications, and the GPU all at once. The bus is narrower and shared.</p>
<p>In practice: GDDR7 delivers ~1,792 GB/s dedicated to the GPU. LPDDR5X delivers ~256 GB/s that still need to be split between CPU and GPU. LLM inference is basically &ldquo;read all the model weights from memory, multiply by the current token, generate the next token, repeat.&rdquo; Whoever reads faster, generates faster. There&rsquo;s no shortcut.</p>
<p>The Strix Halo&rsquo;s advantage isn&rsquo;t speed. It&rsquo;s capacity. 96GB of VRAM in a 100W chip that costs a fraction of a professional GPU. The RTX 5090 is 7x faster, but it&rsquo;s stuck at 32GB. Models that don&rsquo;t fit, don&rsquo;t run.</p>
<h2>The alternatives: who else does this?<span class="hx:absolute hx:-mt-20" id="the-alternatives-who-else-does-this"></span>
    <a href="#the-alternatives-who-else-does-this" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>If 96GB isn&rsquo;t enough or you want more speed, the options are limited.</p>
<p>The Framework Desktop uses the same AI Max+ 395 chip with up to 128GB of RAM. Same platform, same performance, but with the differential of being modular and repairable (it&rsquo;s Framework, after all). In practice it&rsquo;s equivalent to the Minisforum on specs and price.</p>
<p>Above that, the alternative is a Mac Studio with M3 Ultra. The M3 Ultra chip supports up to 512GB of unified memory, with ~819 GB/s of bandwidth (more than 3x the Strix Halo). Apple manufactures the memory chips on the package, so latency and bandwidth are superior. You could potentially allocate ~400GB as VRAM and run models that don&rsquo;t fit anywhere outside of professional GPU servers.</p>
<p>Apple&rsquo;s internal NVMe is also another level: ~7.4 GB/s of sequential read on the M3 Ultra, compared with ~14 GB/s on the Crucial T700 (PCIe 5.0). The T700 is faster on raw throughput, but Apple&rsquo;s NVMe latency tends to be lower on random I/O thanks to the SoC integration.</p>
<table>
  <thead>
      <tr>
          <th>Spec</th>
          <th>Minisforum MS-S1 Max</th>
          <th>Mac Studio M3 Ultra (max)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Max RAM</td>
          <td>128 GB LPDDR5X</td>
          <td>512 GB unified</td>
      </tr>
      <tr>
          <td>Allocatable VRAM</td>
          <td>~96 GB</td>
          <td>~400 GB</td>
      </tr>
      <tr>
          <td>Memory bandwidth</td>
          <td>~256 GB/s</td>
          <td>~819 GB/s</td>
      </tr>
      <tr>
          <td>CPU</td>
          <td>Zen 5, 16C/32T</td>
          <td>Apple M3 Ultra, 32C</td>
      </tr>
      <tr>
          <td>GPU compute</td>
          <td>ROCm (gfx1151, experimental)</td>
          <td>Metal (mlx, mature)</td>
      </tr>
      <tr>
          <td>Power draw</td>
          <td>~100W</td>
          <td>~135W</td>
      </tr>
      <tr>
          <td>NVMe</td>
          <td>PCIe 5.0 (standard slot)</td>
          <td>Custom Apple (~7.4 GB/s)</td>
      </tr>
      <tr>
          <td>Price (US)</td>
          <td>~$1,500-2,000</td>
          <td>~$9,999 (512GB config)</td>
      </tr>
      <tr>
          <td>Estimated price (Brazil)</td>
          <td>~R$ 12,000-15,000</td>
          <td>~R$ 110,000+ (imported)</td>
      </tr>
  </tbody>
</table>
<p>The Brazilian price is the elephant in the room. The Mac Studio&rsquo;s max config costs $9,999 in the US. With import taxes (~60% + state ICMS), it goes past R$ 110,000. The Minisforum with 128GB lands at R$ 12,000-15,000. A nearly 8x price gap buys you a lot.</p>
<p>If you need more than 96GB of VRAM for truly enormous models (DeepSeek-V3 with 671B parameters fits in ~400GB Q4, for example), the Mac Studio with 512GB is the only consumer option. The alternative would be professional NVIDIA A6000 GPUs (48GB VRAM, ~$6,000 each, and you&rsquo;d need several in NVLink). For everything that fits in 96GB, the Minisforum gets the job done at a fraction of the cost.</p>
<h2>And projects that promise to run big LLMs on small GPUs?<span class="hx:absolute hx:-mt-20" id="and-projects-that-promise-to-run-big-llms-on-small-gpus"></span>
    <a href="#and-projects-that-promise-to-run-big-llms-on-small-gpus" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>There&rsquo;s a concept called &ldquo;layer offloading&rdquo; that projects like llama.cpp already support. The idea: if the model doesn&rsquo;t fit fully in VRAM, keep some layers on the GPU and the rest in system RAM. The GPU processes the layers it has, hands off to the CPU to process the rest, and back.</p>
<p>In practice, it doesn&rsquo;t work well. The bottleneck is PCIe: transfer speed between system RAM and GPU VRAM is ~32 GB/s (PCIe 5.0 x16). Each generated token needs to transfer data back and forth. The result is that you drop from 150 tok/s (everything in VRAM) to 2-8 tok/s (partial offload). It&rsquo;s too slow for interactive use.</p>
<p>VRAM is the fundamental limitation because LLM inference is memory-bandwidth-bound, not compute-bound. The GPU has compute to spare. What&rsquo;s missing is the ability to read the model weights fast enough. When part of the weights live in system RAM through PCIe, the entire pipeline waits on the transfer.</p>
<p>That&rsquo;s why unified memory (like in the Strix Halo or Apple Silicon) makes a difference. There&rsquo;s no PCIe in the middle. CPU and GPU access the same physical memory. The Strix Halo&rsquo;s 256 GB/s is slow compared to GDDR7, but it&rsquo;s 8x faster than offloading through PCIe.</p>
<h2>Advances in LLM optimization (up to 2026)<span class="hx:absolute hx:-mt-20" id="advances-in-llm-optimization-up-to-2026"></span>
    <a href="#advances-in-llm-optimization-up-to-2026" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>To understand why some models run so much better than others on the Strix Halo, you have to understand what&rsquo;s changed in the ecosystem over the last two years.</p>
<h3>Mixture of Experts (MoE)<span class="hx:absolute hx:-mt-20" id="mixture-of-experts-moe"></span>
    <a href="#mixture-of-experts-moe" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>If you run local models, MoE is the advance that matters most. An MoE model has high total parameters (e.g. 122B in qwen3.5:122b), but only activates a fraction of them per token (e.g. ~4B). The inactive weights stay in VRAM but aren&rsquo;t read on every token, which drastically reduces the bandwidth needed.</p>
<p>In the Strix Halo benchmarks, MoE models run 3-10x faster than dense models of the same size. qwen3.5:35b (MoE, ~4B active) runs at 43 tok/s while qwen2.5:72b (dense, 72B active) runs at 4.5 tok/s.</p>
<h3>DeepSeek and training optimization<span class="hx:absolute hx:-mt-20" id="deepseek-and-training-optimization"></span>
    <a href="#deepseek-and-training-optimization" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>DeepSeek V3 (December 2024) showed that it was possible to train 671B parameter models at a cost an order of magnitude lower than predicted. They combined MoE with FP8 quantization during training (not just inference), multi-stage training with curriculum learning, and several inter-GPU communication optimizations. The impact: everyone copied. Qwen, GLM, MiniMax, all of them adopted variations of the technique.</p>
<h3>Quantization: from FP16 to Q4 without losing much<span class="hx:absolute hx:-mt-20" id="quantization-from-fp16-to-q4-without-losing-much"></span>
    <a href="#quantization-from-fp16-to-q4-without-losing-much" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Quantization compresses model weights from 16 bits (FP16) to smaller formats: 8 bits (Q8), 4 bits (Q4), or even 2 bits. A 70B model that would take ~140GB in FP16 fits in ~40GB at Q4_K_M. Quality loss exists, but in modern formats (GGUF Q4_K_M, AWQ, EXL2) it&rsquo;s small enough for practical use.</p>
<p>GGUF (the llama.cpp format) became the standard for local inference. AWQ and GPTQ are alternatives with more sophisticated calibration, but the ecosystem converged on GGUF because it works on CPU, CUDA and ROCm without recompilation.</p>
<h3>Distillation: smaller models that know more<span class="hx:absolute hx:-mt-20" id="distillation-smaller-models-that-know-more"></span>
    <a href="#distillation-smaller-models-that-know-more" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Distillation is training a small model using the responses of a large model as the teacher. Microsoft&rsquo;s Phi-4 (14B) was trained with distillation from GPT-4 and competes with 70B models on several benchmarks. Qwen3 did the same: qwen3:14b is surprisingly capable for its size.</p>
<h3>Flash Attention and optimized KV Cache<span class="hx:absolute hx:-mt-20" id="flash-attention-and-optimized-kv-cache"></span>
    <a href="#flash-attention-and-optimized-kv-cache" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Flash Attention (Tri Dao, 2022) changed how attention is computed: instead of materializing the full attention matrix in memory, it processes in blocks keeping the data in the GPU&rsquo;s on-chip SRAM, reducing memory consumption from O(n²) to O(n). Without that, contexts of 128K+ tokens would be impractical. It already went through versions 2 and 3, with optimizations for FP8 and async operations on H100. PagedAttention (vLLM, UC Berkeley) did the same for the KV cache during serving: applies virtual memory concepts to the cache, eliminating fragmentation and improving throughput by 2-4x.</p>
<p>In Ollama, I set <code>OLLAMA_FLASH_ATTENTION=1</code> and <code>OLLAMA_KV_CACHE_TYPE=q8_0</code> on the server. The first activates flash attention, the second uses 8-bit KV cache instead of fp16, cutting the bandwidth needed per token in half. These are zero-hardware-cost optimizations that improve throughput measurably.</p>
<h3>What Qwen, Kimi, MiniMax and GLM are doing<span class="hx:absolute hx:-mt-20" id="what-qwen-kimi-minimax-and-glm-are-doing"></span>
    <a href="#what-qwen-kimi-minimax-and-glm-are-doing" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Qwen (Alibaba) has consistently been the best price/performance in open source models. Qwen3:14b is dense and strong; Qwen3.5:122b is MoE and runs surprisingly well in 96GB. GLM-4.7 (Zhipu AI) is notable for offering bf16 full precision versions that fit in 96GB. MiniMax experimented with long contexts (up to 4M tokens). Kimi (Moonshot AI) focused on large context windows with linear architectures.</p>
<h3>What runs well in 96GB of VRAM<span class="hx:absolute hx:-mt-20" id="what-runs-well-in-96gb-of-vram"></span>
    <a href="#what-runs-well-in-96gb-of-vram" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>With 96GB on the Strix Halo, the models that work well for daily use:</p>
<table>
  <thead>
      <tr>
          <th>Model</th>
          <th>Size</th>
          <th>tok/s</th>
          <th>Use</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>qwen3.5:35b</td>
          <td>24 GB</td>
          <td>43.2</td>
          <td>General purpose, excellent</td>
      </tr>
      <tr>
          <td>qwen3-coder-next</td>
          <td>52 GB</td>
          <td>29.5</td>
          <td>Code, MoE</td>
      </tr>
      <tr>
          <td>qwen3.5:122b</td>
          <td>81 GB</td>
          <td>19.2</td>
          <td>Heavy but usable</td>
      </tr>
      <tr>
          <td>glm-4.7-flash:bf16</td>
          <td>60 GB</td>
          <td>17.9</td>
          <td>Full precision</td>
      </tr>
      <tr>
          <td>qwen2.5-coder:32b</td>
          <td>20 GB</td>
          <td>10.2</td>
          <td>Code, dense</td>
      </tr>
      <tr>
          <td>deepseek-r1:32b</td>
          <td>20 GB</td>
          <td>7.4</td>
          <td>Reasoning</td>
      </tr>
  </tbody>
</table>
<p>Dense 70B+ models (deepseek-r1:70b, llama3.3:70b) are still blocked by the ROCm bug I mentioned. When it gets fixed, they should run at ~4-6 tok/s, usable for batch but not for interactive chat.</p>
<h2>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I bought the Minisforum to run models that don&rsquo;t fit in any gaming GPU. For that, it works. It&rsquo;s not fast. 19 tok/s on a 122B model isn&rsquo;t the experience you get with Claude or ChatGPT. But it&rsquo;s local, it&rsquo;s private, and it runs on my shelf consuming less power than an old lightbulb.</p>
<p>For people asking about the Mac Studio: if you have the budget, it&rsquo;s the best machine for running local LLMs. 512GB of unified memory, 819 GB/s of bandwidth, mature Metal/mlx ecosystem. You can run DeepSeek-V3 in full Q4. But in Brazil, with import duties, it crosses R$ 110k. The Minisforum with 128GB at R$ 12-15k is the realistic option.</p>
<p>And for people who think you can work around the VRAM limitation with layer offloading: you can&rsquo;t. PCIe is too slow. The model has to fit fully in VRAM for inference to be usable. It&rsquo;s the reason gaming GPUs with 32GB of ultra-fast GDDR7 are still capped on model size, and why the unified memory of the Strix Halo and Apple Silicon changed the equation.</p>
<p>In the <a href="/en/2026/03/31/migrating-my-home-server-with-claude-code/">next post</a> I tell the story of how I migrated the entire home server to the Minisforum using Claude Code, the problems I ran into, and how openSUSE MicroOS behaves as a Docker server operating system.</p>
]]></content:encoded><category>hardware</category><category>local-models</category><category>performance</category><category>reviews</category></item><item><title>AI Agents: Is GLM 4.7 Flash really that good?</title><link>https://akitaonrails.github.io/en/2026/01/22/ai-agents-is-glm-4-7-flash-really-that-good/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/01/22/ai-agents-is-glm-4-7-flash-really-that-good/</guid><pubDate>Thu, 22 Jan 2026 13:41:05 -0300</pubDate><description>&lt;p&gt;I recently wrote &lt;a href="https://akitaonrails.com/en/2026/01/11/ai-agents-comparing-top-llms-on-the-zig-challenge/"target="_blank" rel="noopener"&gt;a post comparing several LLMs&lt;/a&gt;. The good news is that commercial LLMs are genuinely impressive. The bad news is that no open source LLM was able to complete my coding challenge (read the previous post to understand the challenge).&lt;/p&gt;
&lt;p&gt;Also recently, ZAI released version 4.7 of their famous &lt;a href="https://huggingface.co/zai-org/GLM-4.7"target="_blank" rel="noopener"&gt;&amp;ldquo;GLM&amp;rdquo;&lt;/a&gt; model, a 30 billion parameter model encoded in BF16, needing around 60GB of VRAM to fit entirely on a GPU. So it&amp;rsquo;s heavy.&lt;/p&gt;</description><content:encoded><![CDATA[<p>I recently wrote <a href="https://akitaonrails.com/en/2026/01/11/ai-agents-comparing-top-llms-on-the-zig-challenge/"target="_blank" rel="noopener">a post comparing several LLMs</a>. The good news is that commercial LLMs are genuinely impressive. The bad news is that no open source LLM was able to complete my coding challenge (read the previous post to understand the challenge).</p>
<p>Also recently, ZAI released version 4.7 of their famous <a href="https://huggingface.co/zai-org/GLM-4.7"target="_blank" rel="noopener">&ldquo;GLM&rdquo;</a> model, a 30 billion parameter model encoded in BF16, needing around 60GB of VRAM to fit entirely on a GPU. So it&rsquo;s heavy.</p>
<p>Even more recently, they released the <a href="https://huggingface.co/zai-org/GLM-4.7-Flash"target="_blank" rel="noopener"><strong>GLM 4.7 Flash</strong></a> version, which is the same 30 billion parameter model but quantized (roughly: &ldquo;compressed&rdquo; or &ldquo;truncated and re-normalized&rdquo;), which fits on a GPU with at least 32GB, which is the case with my <strong>RTX 5090</strong>.</p>
<p>Yes, the most expensive consumer GPU on the market is the bare minimum to run the best open source models. That&rsquo;s because what matters isn&rsquo;t processing power but the amount of VRAM. That&rsquo;s why a Mac Studio, with a technically &ldquo;weaker&rdquo; GPU, has an advantage in LLMs because it shares memory with the RAM, with a theoretical max of 512GB. So the largest models fit without any problem at all.</p>
<p>Other options are Mini PCs with AMD Ryzen AI Max+ CPUs that can share up to 128GB of VRAM.</p>
<p>The challenge was to see if I could run GLM 4.7 Flash locally on my machine.</p>
<blockquote>
  <p><strong>TL;DR: GLM 4.7 Flash is possibly the best open source LLM — with a few caveats.</strong></p>

</blockquote>
<h3>VLLM vs LM Studio vs Ollama<span class="hx:absolute hx:-mt-20" id="vllm-vs-lm-studio-vs-ollama"></span>
    <a href="#vllm-vs-lm-studio-vs-ollama" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>On their <a href="https://huggingface.co/zai-org/GLM-4.7-Flash"target="_blank" rel="noopener">official page</a> there&rsquo;s documentation on how to run it in VLLM and I&rsquo;ll warn you up front: it doesn&rsquo;t work. It requires features that only exist on the master branch of VLLM and transformers and you&rsquo;ll get a headache with version conflicts and dependency resolution, and even then, in the end, I couldn&rsquo;t get it to run properly.</p>
<p>LM Studio doesn&rsquo;t work either. It has an option to download the model, and I tried tuning every possible parameter. KV Cache quantization, GPU layers, CPU threads, etc etc. But it doesn&rsquo;t matter, the model loads but runs absurdly slow. So slow it&rsquo;s unusable. I think it&rsquo;s because it can only download the BF16 model, so half the layers don&rsquo;t fit on the GPU. Therefore, as of the date of this post, neither LM Studio nor VLLM is usable.</p>
<p>But then I saw a <a href="https://x.com/ollama/status/2013372316021834086"target="_blank" rel="noopener">tweet</a> saying Ollama would finally support GLM 4.7, but again, as of the date of this post, it&rsquo;s still not in the stable release. The only way to use it is by compiling from source the bits that are on their master branch.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git clone https://github.com/ollama/ollama.git
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> ollama
</span></span><span class="line"><span class="cl">cmake -B build
</span></span><span class="line"><span class="cl">cmake --build build
</span></span><span class="line"><span class="cl">go build -v .</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>On the last command it will look like it&rsquo;s stuck because nothing shows on the screen, but that&rsquo;s because it&rsquo;s compiling hundreds of C++ files. Just wait and eventually it finishes. And finally you can run it:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">OLLAMA_FLASH_ATTENTION</span><span class="o">=</span><span class="m">1</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="nv">GGML_VK_VISIBLE_DEVICES</span><span class="o">=</span>-1 <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="nv">OLLAMA_KV_CACHE_TYPE</span><span class="o">=</span>q4_0 <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="nv">OLLAMA_CONTEXT_LENGTH</span><span class="o">=</span><span class="m">45000</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">go run . serve</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>These are the parameters to fit everything in the 32GB of VRAM without offloading to the CPU. 45 thousand tokens is the max, maybe even a bit less to make sure the KV Cache fits.</p>
<p>On top of that you need to quantize to 4-bits with <code>q4_0</code>. Normal is 16-bit floats. To fit we&rsquo;ll have to truncate (lose precision).</p>
<p>In my case, I have an AMD iGPU. To prevent Ollama from trying to use it, there&rsquo;s this option <code>GGML_VK_VISIBLE_DEVICES=-1</code>. That should guarantee only the nvidia gets used.</p>
<p>Finally, we enable <strong>Flash Attention</strong> to try to reduce memory growth as the context window grows.</p>
<h3>Trade-off: speed vs context<span class="hx:absolute hx:-mt-20" id="trade-off-speed-vs-context"></span>
    <a href="#trade-off-speed-vs-context" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>GLM 4.7 Flash is a model with 48 layers. With the configuration I showed above, all 48 layers fit on the GPU. But a 45k context window <strong>can</strong> be too small depending on your use case. You&rsquo;ll have to test for yourself.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260123101949_screenshot-2026-01-23_10-09-35.png" alt="log gpu"  loading="lazy" /></p>
<p>When everything fits on the GPU, processing is reasonably fast. A bit slower but still comparable to Qwen3 Coder or GPT-OSS. You can tell everything&rsquo;s on the GPU because looking at BTOP the CPU is doing nothing while <code>nvidia-smi</code> shows the GPU pulling around 500W out of the 600W it can draw.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260123102147_screenshot-2026-01-22_18-50-42.png" alt="nvidia-smi 500w"  loading="lazy" /></p>
<p>In my tests, depending on what you&rsquo;re doing, those 45k fill up <strong>VERY FAST</strong>. Using tools like OpenCode or Crush, when the context is close to full, they ask the LLM to summarize what&rsquo;s been discussed/researched so far, save it, and restart with the summary. But even so, the summaries themselves grow large.</p>
<p>With agents, the model runs commands on your system (compile, list files, search code snippets, search the web) and all of that consumes context. The more context you have left, the better.</p>
<p>I tried running with <code>OLLAMA_CONTEXT_LENGTH=65576</code>, around 65k tokens. That prevents all layers from staying on the GPU and Ollama ends up swapping, using the CPU. So processing gets much slower. BUT, the context window gets bigger. It&rsquo;s a trade-off.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260123102557_screenshot-2026-01-22_20-51-05.png" alt="btop"  loading="lazy" /></p>
<p>Right off the bat you can see the CPU starts spinning up. On my 7850x3d it&rsquo;s running at 50% constantly. Look at how slow Ollama response times got:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260123102645_screenshot-2026-01-23_10-09-50.png" alt="ollama cpu"  loading="lazy" /></p>
<p>Goes from around 4 seconds to 90 seconds. 20x slower. And that&rsquo;s with 39 of the 48 layers still on the GPU. That&rsquo;s why I don&rsquo;t think you can push beyond 65k. The ideal is to stay at 45k, but for my challenge I felt I needed to compensate.</p>
<p>Either Crush/OpenCode gets slow because the window runs out too fast and it has to summarize the work constantly and lose context details — which means extra work re-researching the same thing more than once. Or you preserve more context details for longer, but then every response gets slower. It&rsquo;s a really tough trade-off. And it&rsquo;s all the fault of these crappy video cards having so little VRAM.</p>
<p>If you use a Mac Studio that goes up to 512GB or some Mini-PC with AMD Ryzen AI Max+ that goes up to 128GB, we wouldn&rsquo;t have to deal with this limit. But those are machines that cost over BRL 20k. You can&rsquo;t justify it. For that price it&rsquo;s better to just pay credits on OpenRouter and use larger, faster commercial models like Claude Opus or GPT 5.2, or GLM 4.7 itself on ZAI&rsquo;s cloud.</p>
<h3>Pull Request on Crush<span class="hx:absolute hx:-mt-20" id="pull-request-on-crush"></span>
    <a href="#pull-request-on-crush" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>As a side quest, I had a small problem that was bothering me. One of the problems with a small context window is that every once in a while Crush would just stop, even though there was still work to do. The reason is that the context fills up before it has time to summarize, so when it goes to make a tool call, which is a big XML blob, it gets truncated for lack of space.</p>
<p>Truncated XML is invalid, it can&rsquo;t call the tool and the processing crashes, like this:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260123103238_screenshot-2026-01-22_15-12-36.png" alt="log xml"  loading="lazy" /></p>
<p>And even trying to control the memory usage ceiling, eventually that ceiling breaks and processing halts for lack of memory:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260123103736_screenshot-2026-01-23_10-36-43.png" alt="log nvm"  loading="lazy" /></p>
<p>Whether from truncated XML or Ollama OOM, processing eventually stops. And since it&rsquo;s slow processing, it&rsquo;s a pain having to wait for this to happen and manually restart the process.</p>
<p>So I decided to see if I could make Crush itself detect that it stopped for these reasons and auto-continue from where it left off. I opened Crush on top of the Crush project itself and asked Claude Opus to check the possibility of a fix.</p>
<p><a href="https://github.com/charmbracelet/crush/pull/1962"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260123105702_screenshot-2026-01-23_10-56-40.png" alt="pull request"  loading="lazy" /></a></p>
<p>Yes, I made a 100% vibe coded pull request using Crush itself to fix Crush 😂 Let&rsquo;s see if the Crush folks accept it. I tried my best not to produce <a href="https://www.youtube.com/watch?v=vrTrOCQZoQE&amp;pp=ygUHYWkgc2xvcNIHCQmHCgGHKiGM7w%3D%3D"target="_blank" rel="noopener">AI Slop</a>.</p>
<p>Either way, with this I can leave the agent running for hours without stopping until it gets to where I want.</p>
<h3>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><blockquote>
  <p>This is NOT the right way!</p>

</blockquote>
<p>Let me be clear that leaving an agent doing things on its own with no one watching is bad practice. In my case it&rsquo;s an isolated exercise. I&rsquo;m using my <a href="/en/2026/01/10/ai-agents-locking-down-your-system/">AI JAIL</a> and I&rsquo;ve run this prompt many times as a benchmark and I know the behavior of all the LLMs well.</p>
<p>As I said in my previous post, my challenge prompt is full of bad practices you shouldn&rsquo;t follow: I ask for many complex things at once, with little context, in a vague and confusing way, precisely to see how the LLM will handle it. Day-to-day you should send small prompts, objective unitary tasks, well specified, with good explanation and context, so the responses are fast and short. One step at a time.</p>
<p>The goal of my challenge is precisely to push the limits of common sense a bit. With the idea that if it passes my challenge, it should be able to handle simpler day-to-day prompts.</p>
<p>And in this challenge only the commercial LLMs got through: Claude Opus, GPT 5.2, Gemini 3. No open source model made it, not Qwen3 Coder, not GPT-OSS, not my first attempt with GLM 4.7 cloud, nor MiniMax v2.</p>
<p>But this new GLM 4.7 Flash gave me hope. It didn&rsquo;t go into an &ldquo;agentic loop&rdquo; like Qwen3 — which always reaches a point where it stops trying to solve and just repeats &ldquo;I&rsquo;m done&rdquo; without being done.</p>
<p>It also didn&rsquo;t crash for no reason. Every time processing stopped, it was out of memory. And even so, sending &ldquo;continue&rdquo; via prompt in Crush, it manages to pick up where it left off and keep going.</p>
<p>Every fix it makes makes sense. I didn&rsquo;t see it going off on tangents outside the scope like I saw Gemini doing many times. When it has doubts, it can request an &ldquo;agentic fetch&rdquo; and go search the web before continuing. Every time it makes fixes it knows to run the build command to check for compilation errors.</p>
<p>I liked that it tries to fix one small error at a time instead of rewriting everything or solving more than one problem at once.</p>
<blockquote>
  <p>In the end, it was the only open source one that got the build working and produced a binary.</p>

</blockquote>
<p>I couldn&rsquo;t have it test the program because it would need to load another LLM on a GPU that was already hosting an LLM, so I stopped the test without resolving runtime bugs. And I also didn&rsquo;t ask it to refactor because it had already taken too long. It had already surpassed the other OSS ones so I stopped there. Here&rsquo;s the <a href="https://github.com/akitaonrails/qwen-cli-zig/pull/6"target="_blank" rel="noopener">pull request</a> with the build fixes it made. Simple, not touching things it shouldn&rsquo;t, doing what was asked.</p>
<p>So of the open source models, this was the best behavior I saw, comparable to what I&rsquo;ve seen Claude Opus or GPT 5.2 doing. Considering those commercial models have trillions of parameters, running on the beefiest NVIDIA H200 servers, I consider this GLM, at just 30 billion parameters, running on a measly local 5090, to be doing impressively well.</p>
<p>The trade-offs are clear: it&rsquo;s orders of magnitude slower than Claude Opus. But a slow model that gets to the same result beats a fast one that goes into an infinite loop thinking it solved everything without having done anything, like Qwen3.</p>
<p>I think for challenges like mine — a giant prompt that needs a lot of context to solve — no open source model is good enough. But if I were starting a small project from scratch, with simple and short tasks, I imagine GLM 4.7 would handle it without any headaches, costing zero and preserving my privacy by running 100% locally and offline.</p>
<p>My recommendation: GLM 4.7 Flash is the best OSS model of early 2026.</p>
]]></content:encoded><category>llm-benchmarks</category><category>local-models</category><category>coding-agents</category></item><item><title>Dissecting an Ollama Modelfile - Tuning Qwen3 for Code</title><link>https://akitaonrails.github.io/en/2025/04/29/dissecting-an-ollama-modelfile-tuning-qwen3-for-code/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2025/04/29/dissecting-an-ollama-modelfile-tuning-qwen3-for-code/</guid><pubDate>Tue, 29 Apr 2025 02:30:00 -0300</pubDate><description>&lt;p&gt;The hot news of the day is the release of the new Qwen3 model. I already &lt;a href="https://www.akitaonrails.com/en/2025/04/28/testando-o-recem-lancado-llm-open-source-qwen3-com-aider-e-ollama"target="_blank" rel="noopener"&gt;posted about it today&lt;/a&gt; and was quite impressed. I thought I would leave it at that, but then I saw this tweet:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/1jgq3ar5c5l7wfjt9wjfui8l598z" alt="ivan qwen tweaks" loading="lazy" /&gt;&lt;/p&gt;
&lt;p&gt;See the &lt;a href="https://x.com/ivanfioravanti/status/1916934241281061156"target="_blank" rel="noopener"&gt;full text at this link&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;He gives tips on how to &amp;ldquo;tune&amp;rdquo; this new model, mostly for math-related questions, and I started wondering whether we can tune it for software development. Before more advanced researchers yell at me: this is my first time messing with the parameters of an LLM, so if you want to add context in the comments, you&amp;rsquo;re welcome.&lt;/p&gt;</description><content:encoded><![CDATA[<p>The hot news of the day is the release of the new Qwen3 model. I already <a href="https://www.akitaonrails.com/en/2025/04/28/testando-o-recem-lancado-llm-open-source-qwen3-com-aider-e-ollama"target="_blank" rel="noopener">posted about it today</a> and was quite impressed. I thought I would leave it at that, but then I saw this tweet:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/1jgq3ar5c5l7wfjt9wjfui8l598z" alt="ivan qwen tweaks"  loading="lazy" /></p>
<p>See the <a href="https://x.com/ivanfioravanti/status/1916934241281061156"target="_blank" rel="noopener">full text at this link</a></p>
<p>He gives tips on how to &ldquo;tune&rdquo; this new model, mostly for math-related questions, and I started wondering whether we can tune it for software development. Before more advanced researchers yell at me: this is my first time messing with the parameters of an LLM, so if you want to add context in the comments, you&rsquo;re welcome.</p>
<p>Let&rsquo;s go. First, what are these TopP, TopK and everything else Ivan mentions in his tweet? They are <strong>KEY SAMPLING</strong> parameters. It&rsquo;s a group of parameters that control <em>HOW</em> the model selects the <em>NEXT TOKEN</em> and goes on completing the text (giving the &ldquo;answer&rdquo;), based on its vocabulary and the probability distribution it calculates. The most common ones are exactly <code>temperature</code>, <code>top_p</code>, <code>top_k</code> and sometimes <code>min_p</code> or <code>repeat_penalty</code>.</p>
<p>I always say: an LLM is a next-word generator, a text completer based on probabilities. When the LLM processes your input (your context, chat history and the next question), it needs to calculate a probability &ldquo;score&rdquo; for every word or sub-word (token) across its entire vocabulary. Imagine a gigantic model, it needs to recalculate everything all the time, for every next word.</p>
<p>Sampling parameters tell the model &ldquo;how to choose the next token&rdquo; from the list of probabilities. Without these parameters the model would use a &ldquo;greedy&rdquo; decoder (too ambitious). That means it would just pick the token with the highest probability every time. And that leads to a repetitive, unnatural-sounding result. Sampling adds a kind of &ldquo;controlled randomness&rdquo; (remember? I always say there is randomness baked in, the answer is never &ldquo;right&rdquo;), and that makes the response more &ldquo;interesting&rdquo; to a human being (who is easily fooled). Here are some of the parameters:</p>
<h3>Temperature<span class="hx:absolute hx:-mt-20" id="temperature"></span>
    <a href="#temperature" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Controls randomness, which many people call &ldquo;creativity&rdquo;. It essentially makes the distribution hotter (more spread out and flatter) or colder (with more peaks around the same tokens). The higher the temperature (up to 1.0), the more it increases the probability of picking tokens that had lower calculated probabilities. The lower the temperature (down to 0.0), the smaller the chances for tokens with lower probabilities, and the more &ldquo;greedy&rdquo; the result gets (less &ldquo;human&rdquo;).</p>
<p>For code, crank it too high and you start getting &ldquo;hallucinations&rdquo; (which are basically increased randomness) and code that doesn&rsquo;t make sense. If it&rsquo;s zero, it gets very &ldquo;boilerplate&rdquo;, repetitive, using very common patterns and can potentially end up in an infinite loop with no answer (because it can&rsquo;t pull anything different).</p>
<p>The tweet suggests 0.6, which is moderate. But in my tests, a temperature that high already started giving me some weird code. Out of nowhere it would change a variable name. Out of nowhere a Chinese character would appear in the middle of the code (literally). I think 0 is too low, but 0.1 is enough, maybe 0.2.</p>
<p>You can change this in the <code>~/.aider.model.settings.yaml</code> file before loading the model (these parameters can be readjusted at startup):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl">- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">ollama_chat/qwen3-dev:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">extra_params</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">temperature</span><span class="p">:</span><span class="w"> </span><span class="m">0.1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">num_ctx</span><span class="p">:</span><span class="w"> </span><span class="m">40960</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>top_p (Nucleus Sampling)<span class="hx:absolute hx:-mt-20" id="top_p-nucleus-sampling"></span>
    <a href="#top_p-nucleus-sampling" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Here it starts to get complicated. This <code>top_p</code> is used to select tokens from the smallest set of tokens whose cumulative probability exceeds this &ldquo;p&rdquo; limit. The model then samples only from this reduced set.</p>
<p>The model sorts all the tokens by their probabilities in descending order (from highest to lowest). Then it starts adding up their probabilities until the sum is greater than or equal to <code>top_p</code>. All the tokens included in the sum form the so-called &ldquo;nucleus&rdquo;, and from that set it samples the next token. So it&rsquo;s a draw among the tokens with the sum(x) highest probabilities, more or less. &ldquo;Pick one of the winners, but not necessarily THE winner (which would be greedy)&rdquo;.</p>
<h3>top_k<span class="hx:absolute hx:-mt-20" id="top_k"></span>
    <a href="#top_k" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Consider only the &ldquo;k&rdquo; most likely tokens. The model samples only from this reduced set of the top K highest probability tokens. That really means &ldquo;pick one out of the Top 10 best&rdquo;. The higher the K, the greater the diversity, the lower it goes, the more greedy.</p>
<p>And this is relative, top_k and top_p work together. This configuration helps anchor the selection to the &ldquo;most likely&rdquo; tokens while still allowing top_p to filter out the lower probability ones. It prioritizes plausibility (top_p) while restricting the total number of sampled tokens (top_k). It helps keep the result focused on the most likely &ldquo;correct&rdquo; answers (but again, it&rsquo;s a draw/sampling).</p>
<h3>min_p<span class="hx:absolute hx:-mt-20" id="min_p"></span>
    <a href="#min_p" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Filters out tokens whose probability is less than &ldquo;min_p&rdquo; times the probability of the most likely token. Before sampling, the model identifies the token with the highest probability (P_max), then removes any token with a probability (P_token) that is lower than <code>P_max * min_p</code>. Sampling then happens over the remaining list (which will be further filtered by top_p and top_k).</p>
<p>This prevents the model from choosing tokens that are &ldquo;extremely unlikely&rdquo; relative to the most obvious choice.</p>
<h3>repeat_penalty<span class="hx:absolute hx:-mt-20" id="repeat_penalty"></span>
    <a href="#repeat_penalty" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>This discourages the model from repeating the same tokens that have already appeared recently in the conversation history or generated text.</p>
<p>Before sampling, the probability score of tokens that have been seen recently is reduced, dividing by this <code>repeat_penalty</code> (with values greater than 1, since dividing by 1 is itself, so it keeps the same probability).</p>
<p>If you leave it at 1.0, it can start repeating the same answers or the same code, even the wrong ones. If it&rsquo;s too high, well above 1.0, the response can sound unnatural, since sometimes it really needs to repeat something to re-explain. A moderate value somewhere around 1.1 up to just under 1.2 tends to be more appropriate so it doesn&rsquo;t punish too much what&rsquo;s already in the history.</p>
<h3>num_predict (max tokens to generate)<span class="hx:absolute hx:-mt-20" id="num_predict-max-tokens-to-generate"></span>
    <a href="#num_predict-max-tokens-to-generate" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The absolute maximum number of tokens the model will generate as a response. If it goes over, it truncates. If it&rsquo;s too small, especially for a code response - which can be long - it can truncate too much and leave the answer incomplete. If it&rsquo;s too big it will greatly increase processing and demand way more machine resources and more time, and it can be wasteful.</p>
<p>A value of around 32 thousand tokens leaves plenty of room for tokens. You have to test, and it varies case by case for the type of response you expect and what each model supports.</p>
<h3>Recommended<span class="hx:absolute hx:-mt-20" id="recommended"></span>
    <a href="#recommended" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Knowing all this now you can also read Ivan&rsquo;s tweet and understand it. He recommends Temp 0.6, TopP 0.95, TopK 20, MinP 0.</p>
<p>I fiddled with it a bit more. And I think that for code, Temp has to be in the 0.1 or 0.2 range. I also think a <code>repeat_penalty</code> of 1.1 is worth it to avoid unnecessary repetitions, especially of code. Again, you have to test. These are empirical numbers, not exact ones.</p>
<p>I created a <a href="https://github.com/akitaonrails/qwen3-dev"target="_blank" rel="noopener">project on my GitHub</a> with a complete model file that not only implements these parameters but also pre-configures a system prompt to bias the model towards being a code assistant first. Here&rsquo;s my prompt:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>SYSTEM &#34;&#34;&#34;
You are a highly functional AI assistant specializing in software development tasks.
Respond to queries about coding, algorithms, debugging, system design,
and programming languages/frameworks.
Provide clear, accurate, step-by-step reasoning and functional code examples.
Always format code snippets using markdown triple backticks (```language ... ```).
Aim for comprehensive and correct technical assistance.
Never suggest changing code unrelated to the question topic at hand.
Restrict yourself to the minimum amount of changes that do resolve the problem at hand.
Avoid renaming functions or variables unless absolutely necessary, 
and in doing so make sure if there&#39;s anyone calling the old names, 
for them to be renamed to the new names. Never leave the code in a broken state. 
Pay close attention to correctness, not just answering the quickiest and dirtiest code to solve the problem.
&#34;&#34;&#34;</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>To make this file, first I pulled the original Qwen3 Modelfile with this command:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>ollama show qwen3 --modelfile &gt; Modelfile</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>It&rsquo;s important to do this because, from what I understand, modelfiles don&rsquo;t inherit from a previous one. They don&rsquo;t work like a &ldquo;FROM&rdquo; in a Dockerfile, where you continue a new image from a previous base.</p>
<p>After editing it with my adjustments - which is already on my GitHub, just create a new model:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>ollama create qwen3-dev -f qwen3-dev.modelfile</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And it will show up in your local ollama. Check with <code>ollama list</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>❯ ollama list
NAME                                            ID              SIZE      MODIFIED
qwen3-dev:latest                                fe03dfae5484    9.3 GB    About an hour ago
qwen3:32b                                       e1c9f234c6eb    20 GB     2 hours ago
qwen3:14b                                       7d7da67570e2    9.3 GB    8 hours ago
qwen3:latest                                    e4b5fd7f8af0    5.2 GB    8 hours ago
qwen2.5-coder:7b-instruct                       2b0496514337    4.7 GB    25 hours ago
....</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>See <code>qwen3-dev:latest</code>? That&rsquo;s our new model with parameter adjustments. In aider just use it directly:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>aider --watch-files --model ollama_chat/qwen3-dev:latest</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Remember to adjust the <code>.aider.model.settings.yml</code> as I showed above.</p>
<p>I haven&rsquo;t done long tests yet, but I decided to play with the 14b model as the base along with these adjustments. The base model already impressed me and at least my adjustments didn&rsquo;t break anything. I kept doing the same refactorings and unit tests on my little educational project and pushed another <a href="https://github.com/akitaonrails/Tiny-Qwen-CLI/pull/6"target="_blank" rel="noopener">pull request</a></p>
<p>I think Gemini 2.5 Pro Exp is a little better, but again, not by a very large margin. And in some cases (like refactoring), I think I prefer the code generated by Qwen3. Compare it with the code I got from Gemini in this <a href="https://github.com/akitaonrails/Tiny-Qwen-CLI/pull/3"target="_blank" rel="noopener">other pull request</a></p>
<p>With Gemini I had to explain less and it gave fewer errors, but both managed to do what I asked all the way through without breaking the main program. Qwen3 looks good enough for me to want to use it. With the advantage that even being the medium model, with 14 billion parameters (which runs on my 24GB of VRAM with room to spare), it still delivers very good and clean code results. And better: it runs MUCH, but MUCH faster than Qwen2.5 32b.</p>
<p>For those who have GPUs with less VRAM, say, a 3070 or 4070 with 16GB, you can use the even smaller 7b model. I tested qwen2.5-coder:7b and had excellent results, so I imagine the Qwen3 7b should be even better and much faster. Speed that is comparable to Claude or Gemini, which are much larger and run on much beefier machines than an RTX 4070.</p>
<p>Running my qwen3-dev, in my monitoring my CPU stayed idle most of the time and the GPU was above 90% the whole time, consuming almost 400W constantly, holding a temperature of 73 degrees (which is good).</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/jv8ub1bc6m70u1agm1nyiooav2mk" alt="btop"  loading="lazy" /></p>
<p>I even had a small incident where my PC shut down out of nowhere, because the temporary extension cord I was using couldn&rsquo;t take it and died. Combined with the CPU and my other devices plugged into the same outlet, it was consistently pulling 1000W from the wall for hours. Playing with A.I. uses <strong>A LOT</strong> of energy, with the air conditioner on because just to dissipate all of this, my apartment gets 1 to 2 degrees (my rough guess) above normal. You can actually feel it a bit warmer lol</p>
<p>But that&rsquo;s it, I hope you got a sense of how things actually work under the hood. There&rsquo;s no magic: it&rsquo;s next-word generation, based on a semi-random draw from probabilities.</p>
<p>Now you know.</p>
]]></content:encoded><category>llms</category><category>local-models</category><category>tutorials</category></item><item><title>Your Own Free Universal Co-Pilot Running Local: AIDER-OLLAMA-QWEN</title><link>https://akitaonrails.github.io/en/2025/04/25/your-own-free-universal-copilot-running-local-aider-ollama-qwen/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2025/04/25/your-own-free-universal-copilot-running-local-aider-ollama-qwen/</guid><pubDate>Fri, 25 Apr 2025 13:40:00 -0300</pubDate><description>&lt;p&gt;In my &lt;a href="https://www.akitaonrails.com/en/2025/04/25/hello-world-de-llm-criando-seu-proprio-chat-de-i-a-que-roda-local"target="_blank" rel="noopener"&gt;previous post&lt;/a&gt; I show how to build an LLM chat from scratch, capable of loading code files for refactoring. I demonstrate the principles behind tools like Co-Pilot, Cursor or Windsurf. The summary is simple:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A GOOD RULES PROMPT&lt;/li&gt;
&lt;li&gt;SCRIPTS that run locally and add more context to the chat session.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That&amp;rsquo;s basically &amp;ldquo;all&amp;rdquo; there is to it (of course, even though the principle is simple, it still takes a lot of work to actually implement). And I don&amp;rsquo;t need to build everything from scratch. There is already an open source alternative that does exactly all of this, the tool that became the most popular over the past months for software development, &lt;a href="https://aider.chat/docs/install.html"target="_blank" rel="noopener"&gt;&lt;strong&gt;AIDER&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;</description><content:encoded><![CDATA[<p>In my <a href="https://www.akitaonrails.com/en/2025/04/25/hello-world-de-llm-criando-seu-proprio-chat-de-i-a-que-roda-local"target="_blank" rel="noopener">previous post</a> I show how to build an LLM chat from scratch, capable of loading code files for refactoring. I demonstrate the principles behind tools like Co-Pilot, Cursor or Windsurf. The summary is simple:</p>
<ul>
<li>A GOOD RULES PROMPT</li>
<li>SCRIPTS that run locally and add more context to the chat session.</li>
</ul>
<p>That&rsquo;s basically &ldquo;all&rdquo; there is to it (of course, even though the principle is simple, it still takes a lot of work to actually implement). And I don&rsquo;t need to build everything from scratch. There is already an open source alternative that does exactly all of this, the tool that became the most popular over the past months for software development, <a href="https://aider.chat/docs/install.html"target="_blank" rel="noopener"><strong>AIDER</strong></a></p>
<p>What I like about AIDER:</p>
<ul>
<li>it works with practically any LLM, closed or open, thanks to using <a href="https://www.litellm.ai/"target="_blank" rel="noopener"><strong>LiteLLM</strong></a> underneath, which abstracts and organizes every configuration like context window size, temperature and other parameters known to work best for each LLM.</li>
<li>it&rsquo;s OPEN SOURCE, so besides being free you can also learn more by reading the source code, like I did.</li>
<li>no need for special - and proprietary - plugins to install in each IDE: it has a &ldquo;watch files&rdquo; feature. So I can leave it open in one terminal pane and my favorite editor in the other pane: NeoVIM. You can integrate with plugins, but it&rsquo;s optional. It will work for everything, even Notepad, if you&rsquo;re masochistic.</li>
</ul>
<p>I tried a bit of that &ldquo;vibe coding&rdquo; crap only through the web interface of ChatGPT, Gemini, Claude, and I&rsquo;ll say that all of them are crap. To build a small project that was simple, with no more than 4 short files, it gave me A LOT of trouble. Some of them:</p>
<ul>
<li>it doesn&rsquo;t take long before it starts mixing the content of one file into another.</li>
<li>mistakes it made, I explained it was wrong, it corrected, but right after it repeats the same thing.</li>
<li>it doesn&rsquo;t run anything, so it guesses a lot, like file paths, library versions</li>
<li>it doesn&rsquo;t matter that the context is big and you can upload many files to context, it&rsquo;s a sliding window: it won&rsquo;t pay attention to everything at the same time. The more code files you upload, the more quality degrades</li>
<li>you have to write a lot to explain what you want and, especially, why it got it wrong and how to fix it.</li>
<li>credits are expensive, they run out really fast, because you waste half the tokens just explaining mistakes and reuploading the same files more than once because you realize it already &ldquo;forgot&rdquo;. I hit the Claude limit, and the Gemini limit, super fast (I pay the first paid plan, I&rsquo;m not going to pay the most expensive one). Then you have to wait a few hours to come back (awful).</li>
</ul>
<p>All of this, someone will comment <em>&ldquo;Oh, but for me it worked.&rdquo;</em> I don&rsquo;t give a damn, I didn&rsquo;t ask. I said that FOR ME it went this way, just accept it. What matters is saying that the whole &ldquo;virtual employee&rdquo; or &ldquo;fully replacing a programmer&rdquo; or &ldquo;building a whole project without knowing how to code at all&rdquo; story is <strong>BALONEY</strong>, <strong>FAIRY TALE</strong>, <strong>CON ARTIST TALE</strong>, <strong>AMATEUR ILLUSION</strong>, etc.</p>
<p>All LLMs are <strong>JUST ANOTHER TOOL</strong> that someone who truly understands, like us programmers, will know how to use much better than any &ldquo;amateur entrepreneur&rdquo; ever will. And I&rsquo;m going to prove it.</p>
<h3>Basic Aider<span class="hx:absolute hx:-mt-20" id="basic-aider"></span>
    <a href="#basic-aider" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Installing Aider is easy, you just need to have Python &gt;= 3.12 on your machine, which every modern Linux has. Their documentation is very good, <a href="https://aider.chat/docs/install.html"target="_blank" rel="noopener">I recommend reading it</a> to know what they recommend post-install. It has many tips.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>python -m pip install aider-install
aider-install</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>What they don&rsquo;t say in the documentation is that the first thing to do is download <a href="https://github.com/Aider-AI/aider/blob/main/aider/website/assets/sample.aider.conf.yml"target="_blank" rel="noopener">this sample file</a> and place it at <code>$HOME/.aider.conf.yml</code>. In it there is one important thing we have to change:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nn">...</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="c">## Enable/disable auto commit of LLM changes (default: True)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">auto-commits</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">...</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>I recommend reading this file, it is fully commented and may have options you want to turn off, but this one is annoying, because - by default - this damn thing does &ldquo;AUTO COMMIT&rdquo; on your Git, every time it changes something in your code. It doesn&rsquo;t even give me a chance to review. It runs on &ldquo;TRUST US&rdquo;. No way, turned off.</p>
<p>This is the kind of thing an amateur wouldn&rsquo;t do, and I even understand. Amateurs don&rsquo;t know how to organize git commits anyway, so probably the automatic Aider messages will be the lesser evil. Look how it ends up:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/22ji8rp2bbw5lsxwbmpneh5clhcr" alt="Aider Git commit"  loading="lazy" /></p>
<p>Heads up to juniors: this is the kind of thing that if I were evaluating, would earn you negative points if I saw it done this way. Do it right: re-check the changes and organize into real commits. Aider &ldquo;says&rdquo; it did a refactor, but it doesn&rsquo;t always get it right, yet it still commits, and then it gets pushed wrong. If the plan is to push everything automatically, even with errors, really, I don&rsquo;t need you.</p>
<p>But I&rsquo;m getting ahead of myself, after installing Aider, you need to configure the API KEYS for each service you use, whether OpenAI, Claude or others. Whoever uses ChatGPT on Linux already has a variable like <code>export OPENAI_API_KEY=sk-proj-........</code> set up in <code>.bashrc</code> or <code>.zshrc</code>.</p>
<p>Once that&rsquo;s done, you can pick which model to use:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code># Change directory into your codebase
cd /to/your/project

# o3-mini
aider --model o3-mini

# o1-mini
aider --model o1-mini

# GPT-4o
aider --model gpt-4o

# List models available from OpenAI
aider --list-models openai/</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And that will already open an interactive chat. You can also, before opening the chat, tell it to load local files like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>aider README.md init.py utils.py ...</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Or, from inside the chat, it accepts <a href="https://aider.chat/docs/usage/commands.html"target="_blank" rel="noopener">several commands</a> starting with slash &ldquo;/&rdquo;, similar to an IRC chat. For example:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>/add helper.py
/copy # copies the last code suggestion to the clipboard
/git # runs a local git command from the chat
/run # runs a shell command and adds the output to the chat
/web # goes to a web page and pulls the content into the chat</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Read the documentation. But the most interesting mode is &ldquo;watch-files&rdquo;. Just be inside your project directory and start it like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>aider --watch-files</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>It uses the Python <a href="https://pypi.org/project/watchfiles/"target="_blank" rel="noopener">watchfiles</a> library to intercept IO calls in that directory and trigger the loading of your files on its own, as soon as you save in your favorite editor (that&rsquo;s why I said no extra plugin is needed if you don&rsquo;t want one). Heads up that this lib doesn&rsquo;t support watching SMB or NFS mounts (yes, I tested it).</p>
<p>In your editor, just create a comment near the code you want it to work on, like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>// can you refactor this return so it has more error checking? AI!
export const getCapitalizedLabel = (name: string): string =&gt; {
  return name
    .replace(/_/g, &#34; &#34;)
    .split(&#34; &#34;)
    .map((word: string) =&gt; word.charAt(0).toUpperCase() &#43; word.slice(1))
    .join(&#34; &#34;)
}</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Put in the comment what you want it to do, and end with the &ldquo;AI!&rdquo; trigger. Then on the other terminal where Aider is loaded, it will notice that the file was saved:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>&gt;

Added src/utils/getCapitalizedLabel.ts to the chat
Processing your request...
...</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And it will do its &ldquo;magic&rdquo; (whose secret I&rsquo;ll tell you in a moment). This is the example of what it did (same results with both o4-mini and Qwen2.5, but it&rsquo;s a pretty trivial example):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="kr">export</span> <span class="kr">const</span> <span class="nx">getCapitalizedLabel</span> <span class="o">=</span> <span class="p">(</span><span class="nx">name</span><span class="o">:</span> <span class="nx">string</span><span class="p">)</span><span class="o">:</span> <span class="nx">string</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="k">typeof</span> <span class="nx">name</span> <span class="o">!==</span> <span class="s1">&#39;string&#39;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">throw</span> <span class="k">new</span> <span class="nx">TypeError</span><span class="p">(</span><span class="s1">&#39;Expected a string&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="nx">name</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/_/g</span><span class="p">,</span> <span class="s2">&#34; &#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="s2">&#34; &#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">word</span><span class="o">:</span> <span class="nx">string</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="k">if</span> <span class="p">(</span><span class="nx">word</span><span class="p">.</span><span class="nx">length</span> <span class="o">===</span> <span class="mi">0</span><span class="p">)</span> <span class="k">return</span> <span class="nx">word</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="k">return</span> <span class="nx">word</span><span class="p">.</span><span class="nx">charAt</span><span class="p">(</span><span class="mi">0</span><span class="p">).</span><span class="nx">toUpperCase</span><span class="p">()</span> <span class="o">+</span> <span class="nx">word</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">})</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="s2">&#34; &#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>A graphical editor like Visual Studio Code, as soon as Aider changes the file, reloads it in the editor and you already see the changes. On NeoVim, autoread usually reloads automatically too, but sometimes I need to issue the &ldquo;:e&rdquo; command to force a reload, but nothing major. And just with that we already have an efficient work workflow.</p>
<p>My favorite editor works exactly the same way, without a plugin slowing it down or conflicting with anything. If I need to do pair programming, I call Aider in another terminal and done, just write ideas as comments, and it sends suggestions. Since I turned off the damn auto-commit, if I don&rsquo;t like it, I just UNDO or <code>git checkout</code> the file and it&rsquo;s back like it was.</p>
<p>More than that. If I notice that the <code>o4-mini</code> model isn&rsquo;t giving good results, I can switch to <code>--model gemini</code> - which tends to be better for code than o4. Or <code>--model claude-3-opus-20240229</code>. Speaking of Claude, as I said before <a href="https://aider.chat/docs/llms/anthropic.html"target="_blank" rel="noopener">read the fucking documentation</a></p>
<p>The documentation says that Aider supports &ldquo;thinking tokens&rdquo; from Sonnet 3.7. There is a config file where we can do &ldquo;fine-tuning&rdquo; for each model, the <code>$HOME/.aider.model.settings.yml</code> where we can add:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl">- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">anthropic/claude-3-7-sonnet-20250219</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">edit_format</span><span class="p">:</span><span class="w"> </span><span class="l">diff</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">weak_model_name</span><span class="p">:</span><span class="w"> </span><span class="l">anthropic/claude-3-5-haiku-20241022</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">use_repo_map</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">examples_as_sys_msg</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">use_temperature</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">extra_params</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">extra_headers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">anthropic-beta</span><span class="p">:</span><span class="w"> </span><span class="l">prompt-caching-2024-07-31,pdfs-2024-09-25,output-128k-2025-02-19</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">max_tokens</span><span class="p">:</span><span class="w"> </span><span class="m">64000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">thinking</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">enabled</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">budget_tokens</span><span class="p">:</span><span class="w"> </span><span class="m">32000</span><span class="w"> </span><span class="c"># Adjust this number</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">cache_control</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">editor_model_name</span><span class="p">:</span><span class="w"> </span><span class="l">anthropic/claude-3-7-sonnet-20250219</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">editor_edit_format</span><span class="p">:</span><span class="w"> </span><span class="l">editor-diff</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>Reasoning<span class="hx:absolute hx:-mt-20" id="reasoning"></span>
    <a href="#reasoning" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Thinking or Reasoning as it is called in the marketing of most commercial LLMs is the &ldquo;chain-of-thought&rdquo; pattern, which is a prompting technique where you ask the LLM not to give an answer, but to describe &ldquo;step by step&rdquo; how it would reach the answer, and then repeat the question telling it to follow those steps and then give an answer.</p>
<p>In practice there are several ways to implement something similar. And one of them is to be Multi-Model. Especially with open source models, like DeepSeek-Coder or Qwen2.5-Coder or Llama3. Some models are better at reasoning - at verbally explaining what has to be done, but they are bad at actually writing the code. And some models are not good at explaining things in detail, but were trained to write good code.</p>
<p>We can use this in our favor and Aider has a feature I think is really badass - for cases where you really need it, because it&rsquo;s heavy - which is running in <strong>architect mode</strong>, generating the reasoning and then switching to <strong>code mode</strong> on another model, and using that reasoning to assemble better code.</p>
<p>There is a <a href="https://aider.chat/2024/09/26/architect.html"target="_blank" rel="noopener">whole article</a> in the Aider documentation explaining this, but the motivation came from the OpenAI o1 model, which is precisely strong at reasoning an explanation of what to do, but bad at actually writing the code. So it&rsquo;s worth listening to o1 and letting it analyze the problem and describe it in text form, and then switching to Gemini Pro or Exp and telling it to write the code.</p>
<p>In practice, just start Aider first in &ldquo;Architect&rdquo; mode:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>pip install -U aider-chat

# Change directory into a git repo
cd /to/your/git/repo

# Work with Claude 3.5 Sonnet as the Architect and Editor
export ANTHROPIC_API_KEY=your-key-goes-here
aider --sonnet --architect</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And this works with other models:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code># Work with OpenAI models, using gpt-4o as the Editor
export OPENAI_API_KEY=your-key-goes-here
aider --4o --architect
aider --o1-mini --architect
aider --o1-preview --architect</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>As an example, setting <code>OPENAI_API_KEY</code> and <code>GEMINI_API_KEY</code> I can launch o4 as architect and Gemini as the coder:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>❯ aider --watch-files --architect --editor-model gemini
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Using gpt-4o model with API key from environment.
Aider v0.82.3.dev55&#43;g25a30393
Main model: gpt-4o with architect edit format
Editor model: gemini/gemini-2.5-pro-preview-03-25 with editor-diff edit format
Weak model: gpt-4o-mini
Git repo: .git with 184 files
Repo-map: using 4096 tokens, auto refresh
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
architect&gt;</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>See in the startup status that it accepted both. Now just code the same way as before, but using both. Obviously, this way you&rsquo;ll burn a lot more credits! Watch out for that! There are <strong>chances</strong> of better results but you <strong>certainly</strong> will spend more credits, so use this more for more complex code, where a single model is struggling to solve it.</p>
<p>Note there are configurations and token limits, and also note that at the end of every response, Aider gives an estimate of how much it&rsquo;s costing you. Here first is the bill from the OpenAI o4 architect:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>...
This should make the function more concise and efficient.


Tokens: 4.4k sent, 246 received. Cost: $0.01 message, $0.01 session.</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And here is the SECOND bill from the Coder Gemini 2.0 Flash:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>...
Tokens: 1.9k sent, 136 received. Cost: $0.0037 message, $0.02 session.
Applied edit to src/utils/getCapitalizedLabel.ts</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Free Gemini has a &ldquo;rate limit&rdquo; (number of requests allowed per minute, super low, only 15 RPM, for an API it&rsquo;s ridiculous, Aider hits it on the first try).</p>
<p>Not rarely, you&rsquo;ll hit the plan limit, and that&rsquo;s a pain:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>The API provider has rate limited you. Try again later or check your quotas.
Retrying in 4.0 seconds...</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Then there&rsquo;s no way around it, you have to go to the <a href="">Google AI Studio</a> site, set up your billing account, add your card and move up to a slightly better paid plan like 2.0 Flash, which supports 2,000 RPM and 4M TPM and the price isn&rsquo;t expensive:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/ozbz5tvmpyla0huinqa6da2iwb1m" alt="Gemini 2.0 Flash"  loading="lazy" /></p>
<p>Some say that today Gemini 2.0 has delivered code results similar or superior to Claude Sonnet 3.7. It varies case by case, it&rsquo;s never an absolute measure, but in my small experience, both are very good and I see both being superior even to ChatGPT 4 or o4. But OpenAI, for me, has really been better at &ldquo;verbal&rdquo; responses, so the strategy of separating the &ldquo;architect&rdquo; role for OpenAI and the &ldquo;coder&rdquo; role for Gemini or Claude makes A LOT of sense.</p>
<p>In the end, that initial code I asked to refactor, in this architect/editor combination came out like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="kr">export</span> <span class="kr">const</span> <span class="nx">getCapitalizedLabel</span> <span class="o">=</span> <span class="p">(</span><span class="nx">name</span><span class="o">:</span> <span class="nx">string</span><span class="p">)</span><span class="o">:</span> <span class="nx">string</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="nx">name</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/_/g</span><span class="p">,</span> <span class="s1">&#39; &#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/\b\w/g</span><span class="p">,</span> <span class="kr">char</span> <span class="p">=&gt;</span> <span class="kr">char</span><span class="p">.</span><span class="nx">toUpperCase</span><span class="p">());</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Ok, I wouldn&rsquo;t have thought of it this way.</p>
<h3>Not using &ldquo;Token Credits&rdquo;: OLLAMA<span class="hx:absolute hx:-mt-20" id="not-using-token-credits-ollama"></span>
    <a href="#not-using-token-credits-ollama" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Now comes the most interesting part for us, more hard-core nerds, with beefier machines (at minimum an RTX 3090, I use an RTX 4090 - what matters is having <strong>24GB of VRAM</strong>). You can use smaller models that fit in less VRAM, but then the code result will be much worse, so you&rsquo;re better off sticking with Gemini or Claude.</p>
<p>If you have the machine for it, with CUDA Toolkit already installed, it&rsquo;s time for the grown-up solution: installing Ollama. On my Manjaro/Arch it goes like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>yay -S ollama ollama_cuda</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Be sure to check your CUDA configuration because by default Ollama will silently run on your CPU and everything will be ABSURDLY SLOW.</p>
<p>Ollama works both as a local HTTP server for APIs, and as an interactive chat client, like my qwen_cli from the previous post. It works with logic more or less similar to Docker. It can be configured to start automatically as a service on your system. See the <a href="https://wiki.archlinux.org/title/Ollama"target="_blank" rel="noopener">ArchWiki documentation</a>.</p>
<p>I prefer to start it manually in a terminal, where I can see the log of what&rsquo;s happening:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>OLLAMA_FLASH_ATTENTION=1 OLLAMA_CONTEXT_LENGTH=8192 ollama serve</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>There are several configurations via environment variables. You can just set them in your <code>/etc/profile</code> or <code>~/.zshrc</code> but since I&rsquo;m testing, I prefer to launch directly on the command line. With this we&rsquo;ll have a web server. To work with Aider, again, just declare where to find it in your <code>/.zshrc</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>export OLLAMA_API_BASE=http://127.0.0.1:11434</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now, we need to download some model, like my favorite Qwen 2.5 Coder, which I already used in the previous post:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>ollama pull qwen2.5-coder:32b</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Note I&rsquo;m explicitly asking for the 32B version, but you can try a lighter one like the 7b. Each model supported by Ollama has <a href="https://ollama.com/library/qwen2.5-coder"target="_blank" rel="noopener">a page</a> describing details of this sort:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/q76sz7qyqupk8jv3gm1igpk4q0i3" alt="qwen 2.5 coder page"  loading="lazy" /></p>
<p>Depending on which synthetic benchmark you &ldquo;believe in&rdquo; you&rsquo;ll see comparisons like this one:</p>
<p><img src="https://ollama.com/assets/library/qwen2.5-coder/bbf378d8-c80e-4ae3-98ab-90111dfbf3e7" alt="comparison"  loading="lazy" /></p>
<p>In practice, Claude, ChatGPT, Gemini, really are already very good for code. In the open source world the options are Qwen 2.5 Coder, Deepseek-Coder-V2 (which still doesn&rsquo;t work very well in Aider, we have to wait for updates). So, if you want everything for free, the best for now is Qwen 2.5 Coder. And in my experience, it has worked very well. But it is VERY heavy in the 32B version and responses are considerably slower than a paid option like Gemini. So it depends a lot on your case.</p>
<p>Once the model is downloaded, you need to configure the context window (by default it&rsquo;s quite small, only 2K tokens), <a href="https://aider.chat/docs/llms/ollama.html"target="_blank" rel="noopener">the documentation warns</a> about this and you have to edit the file <code>$HOME/.aider.model.settings.yml</code> like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl">- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">ollama/qwen2.5-coder:32b</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">extra_params</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">num_ctx</span><span class="p">:</span><span class="w"> </span><span class="m">65536</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>What the documentation does <strong>NOT</strong> explain and, because of that, I spent a long time digging through the source code and Issues on GitHub, is that to launch open source models, you need to use the <code>ollama_chat/</code> option and not <code>ollama</code> as it tells you. That is, the documentation says:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>aider --model ollama/qwen2.5-coder:32b</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>But the CORRECT way is to do:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>aider --model ollama_chat/qwen2.5-coder:32b</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Otherwise, the responses will be completely random, out of context. Pay attention to this. But knowing this, we can even do what I mentioned before: mix models! How about launching GPT o4 as architect and letting it command the editor being Qwen 2.5?</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>aider --watch-files --architect --editor-model ollama_chat/qwen2.5-coder:32</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Yes, this is possible. Now you can use a commercial model you&rsquo;re more used to, like Gemini or Claude, in both architect and editor roles, and an open source model like Qwen 2.5 as support, and pay less. Or just use Qwen 2.5 all the way and pay <strong>zero credits</strong> (only electricity, of course, it will draw <strong>+200W</strong> from the wall every time, but just for a few seconds each time).</p>
<p>In my (little) experience. Gemini still produces better code than Qwen, but that doesn&rsquo;t mean Qwen is bad, as I said, it will depend A LOT on your prompt instructions (the more detailed the better). And speaking of instructions, let&rsquo;s move on to the final part:</p>
<h3>The Secret of the Magic: Prompts<span class="hx:absolute hx:-mt-20" id="the-secret-of-the-magic-prompts"></span>
    <a href="#the-secret-of-the-magic-prompts" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>I make fun when someone talks about &ldquo;prompt engineering&rdquo;, but in reality, the best way to get the most out of an LLM is to make the best possible prompt. This doesn&rsquo;t mean the &ldquo;longest prompt&rdquo;. It&rsquo;s quality and not quantity.</p>
<p>That&rsquo;s why both <a href="https://github.com/microsoft/generative-ai-for-beginners"target="_blank" rel="noopener">Microsoft</a> and <a href="https://www.kaggle.com/whitepaper-prompt-engineering"target="_blank" rel="noopener">Google</a> made detailed guides focused on prompting. Theirs are worth reading and studying. What isn&rsquo;t worth it is paying for courses from random people talking about &ldquo;prompt engineering&rdquo; as if they understood anything.</p>
<p>Aider supports a &ldquo;&ndash;verbose&rdquo; option, where it shows exactly what it is sending to the LLMs. Let&rsquo;s test. Right away it already prints this to the console:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>❯ aider --watch-files --verbose
Config files search order, if no --config:
  - /tmp/smells/.aider.conf.yml
  - /home/akitaonrails/.aider.conf.yml (exists)
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Too soon to check version: 11.6 hours
Command Line Args:   --watch-files --verbose
Config File (/home/akitaonrails/.aider.conf.yml):
  max-chat-history-tokens:8192
  map-tokens:        4096
  auto-commits:      False
Defaults:
  --set-env:         []
  --api-key:         []
  --model-settings-file:.aider.model.settings.yml
  --model-metadata-file:.aider.model.metadata.json
  ...
  --encoding:        utf-8
  --line-endings:    platform
  --env-file:        /tmp/smells/.env

Option settings:
  - 35turbo: False
  - 4: False
  - 4_turbo: False
  ...
  - upgrade: False
  - user_input_color: #00cc00
  - verbose: True
  - verify_ssl: True
  - vim: False
  - voice_format: wav
  - voice_input_device: None
  - voice_language: en
  - watch_files: True
  - weak_model: None
  - yes_always: None
  ...</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>What tools like Aider, Cursor, Co-Pilot and others do is have optimized parameters for every LLM already hard-coded in the tool, like in this case where it detects to use o4 and already configures with better parameters (that you can still tune by putting them in <code>$HOME/.aider.model.settings.yml</code>).</p>
<p>But when you send the first question that gets interesting, look at this:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/rb4hxk4cn181fkkf841snl7ygfja" alt="Aiden Verbose 1"  loading="lazy" /></p>
<p><em>&ldquo;Act as an expert software developer. Always use best practices when coding. Respect and use existing conventions, libraries, etc that are already present in the code base. Take requests for changes to the supplied code. Be diligent and not lazy! You will NEVER leave comments describing code without implementing it! You will always COMPLETELY IMPLEMENT the needed code! &hellip;&rdquo;</em></p>
<p>Remember what I said at the beginning of the post: RULES PROMPTS. This SYSTEM at the beginning is a &ldquo;role&rdquo;. You as user are the &ldquo;USER&rdquo; and the model is the &ldquo;ASSISTANT&rdquo;, and in fact this SYSTEM prompt is gigantic, it scans my project and already explains the project structure to the model and even summarizes some files (showing only the interface of methods, for example, to give context without uploading the whole code).</p>
<p>It has snippets like:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>...
SYSTEM # *SEARCH/REPLACE block* Rules:
SYSTEM
SYSTEM Every *SEARCH/REPLACE block* must use this format:
SYSTEM 1. The *FULL* file path alone on a line, verbatim. No bold asterisks, no quotes around it, no escaping of characters, etc.
SYSTEM 2. The opening fence and code language, eg: ```python
SYSTEM 3. The start of search block: &lt;&lt;&lt;&lt;&lt;&lt;&lt; SEARCH
SYSTEM 4. A contiguous chunk of lines to search for in the existing source code
SYSTEM 5. The dividing line: =======
SYSTEM 6. The lines to replace into the source code
SYSTEM 7. The end of the replace block: &gt;&gt;&gt;&gt;&gt;&gt;&gt; REPLACE
SYSTEM 8. The closing fence: ```
SYSTEM
SYSTEM Use the *FULL* file path, as shown to you by the user.
...</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>In fact, if you&rsquo;ve used Co-Pilot, you know you can put the conventions of your project in <code>CONVENTIONS.MD</code>. Aider will also load that file into the prompt. So things like &ldquo;use 4 spaces instead of tabs, place the opening { at the end of the line and not starting a new line, etc&rdquo;, all of that goes in that file and both Co-Pilot and Aider add these rules to the PROMPT. There&rsquo;s no other mechanism, it&rsquo;s PROMPT.</p>
<p>Then, as USER, it starts uploading summaries of my files in the prompt:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>...
USER Here are summaries of some files present in my git repository.
USER Do not propose changes to these files, treat them as *read-only*.
USER If you need to edit any of these files, ask me to *add them to the chat* first.
USER
USER data_scraper/main.py:
USER ⋮
USER │def main(args: Arguments):
USER ⋮
USER
USER data_scraper/src/arguments.py:
USER ⋮
USER │@dataclass(frozen=True)
USER │class Arguments:
USER │    content_path: Path
USER ⋮
USER │    @staticmethod
USER │    def get() -&gt; &#39;Arguments&#39;:
USER ⋮
...</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>When this part ends, Aider asks for confirmation from the Model:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>...
ASSISTANT Ok, I won&#39;t try and edit those files without asking first.
...</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And that&rsquo;s how a &ldquo;software A.I.&rdquo; works, with a ton of instruction prompts and a client program that keeps parsing the responses and triggers actual commands that run on your system (agents), like git or lint commands. The result of those commands, the text, gets concatenated into the chat to add to the context and then it asks the model/assistant to keep analyzing from that point on.</p>
<p>A model alone does nothing. The one doing the work is the tool that loads the model, in this case Aider, or Co-Pilot, or Cursor. The program comes pre-loaded with several pre-written profiles/personas/roles to add prompts to the model. In the Aider source code, we have this example of <a href="https://github.com/Aider-AI/aider/blob/main/aider/coders/architect_prompts.py"target="_blank" rel="noopener">architect_prompts.py</a></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/nlzkq16pcfjwwow5tbibocr1oa1n" alt="architect role"  loading="lazy" /></p>
<p>Look, in the first variable <code>main_system</code> is that first prompt we saw at the beginning of this section. There are several other pre-programmed profiles whose differences are good to understand. Each model works better with certain types of instructions, with different limitations.</p>
<p>In this other code <a href="https://github.com/Aider-AI/aider/blob/main/aider/commands.py"target="_blank" rel="noopener">commands.py</a> is where we have declared how Aider is able to do things like <code>git commit</code> directly in your project or run the linter of your language:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/bdt1a5dqj28bvzhqximdi8j2gx2g" alt="commands"  loading="lazy" /></p>
<p>None of this is &ldquo;magic&rdquo;, it is all pre-programmed and it - for safety - should only be able to do limited and well-checked commands. We don&rsquo;t want commands that do too much outside the project directory (in fact, NEVER, outside the project directory). Nor should it be able to run many executables. You never know what security bugs might happen when running commands blindly.</p>
<p>If you&rsquo;re in cyber-security, this is where you should audit. But that&rsquo;s the advantage, Aider being open source is auditable. Proprietary and closed tools, no. They run on &ldquo;TRUST THE DADDY&rdquo;.</p>
<p>Test <a href="https://aider.chat/docs/install.html"target="_blank" rel="noopener">Aider</a> today and read that whole documentation site. It is short and has important tips that may help special cases.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/0zeis5fjnkuf2qkmwy7pm6vc62gi" alt="Aider"  loading="lazy" /></p>
<h3>OpenRouter<span class="hx:absolute hx:-mt-20" id="openrouter"></span>
    <a href="#openrouter" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>As you must have realized, the ideal is to test the several commercial LLMs because they evolve and each has strong and weak points. But keeping credits paid in each is a pain.</p>
<p>For this there&rsquo;s <a href="https://openrouter.ai/"target="_blank" rel="noopener">OpenRouter</a> where you have a single account, a single <code>OPEN_ROUTER_API_KEY</code> and pay in one place and it distributes the credits to each provider like OpenAI or Claude as you use them. It simplifies the management of your API costs, it&rsquo;s very worth it and, of course, Aider also knows how to use it:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>aider --model openrouter/openai/gpt-4o</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Again, read <a href="https://aider.chat/docs/llms/openrouter.html"target="_blank" rel="noopener">the documentation</a>.</p>
<p>Now, an <strong>ANTI-PATTERN</strong>. I asked Aider, using the <code>openrouter/google/gemini-2.5-pro-preview-03-25</code> model, which theoretically is the best of the best at the moment, to generate a unit test file for qwen_cli.py, from the little project I dropped on GitHub in yesterday&rsquo;s post.</p>
<p>It failed miserably. It created a test file, almost 800 lines long, stuffed with mocks for everything, testing things that don&rsquo;t need it, overcomplicating. Fine. I try to run it and it throws errors.</p>
<p>A cool feature of Aider is being able to run from inside the chat with <code>/run python -m unittest ...</code> and it offers to throw the errors right into the chat. I ask Gemini to fix it. Then off it goes modifying the test file and my code file. I send <code>/run</code> again, more errors. And I did this about 4 times. It keeps &ldquo;fixing&rdquo; code that didn&rsquo;t need fixing, and simple unit tests (which was just a small typo, just adjust 1 number), it leaves behind. I describe in the prompt <em>&ldquo;you are messing with what you shouldn&rsquo;t and not fixing the trivial thing, like that little number&rdquo;</em> I send <code>/run</code>, and it insists on ignoring and goes off messing around in other places.</p>
<p>Summary:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/q4fiux9vj0d5l6e8p4nmku4rcjma" alt="OpenRouter Credits"  loading="lazy" /></p>
<p>It spent almost 3 bucks and didn&rsquo;t solve the problem. The advantage of OpenRouter is that it all stays centralized in one place. It paid Google for the use of Gemini and deducts from my credits. I had pre-loaded with 100 credits (roughly 100 dollars). And in 30 minutes 3 credits were already gone. Do the math.</p>
<p><strong>Best practice:</strong> the best thing is to open the qwen_cli.py code, for example, in my NeoVIM or any editor and put a comment just on top of a single method:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="c1"># this method is used to just load a config.json file, create a single unit test and add to test_qwen_cli.py, AI!</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">load_config</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Then it makes only <strong>ONE</strong> unit test at a time, we run it with <code>/run</code> and if it passes and the code looks clean, we move on to the next. Doing one method at a time is much more reliable than saying <em>&ldquo;make tests for EVERYTHING at once.&rdquo;</em> That way it&rsquo;s guaranteed we&rsquo;ll waste credits for nothing.</p>
<p>On top of that, one method at a time, open source models like Qwen 2.5 will do well and you run it locally without spending credits.</p>
<h3>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>For now, I&rsquo;m going to adopt Aider. Now I understand why it became so popular this year. It is a well-made little project, open, that I can explore, try to improve and learn more from. At the same time it makes using commercial LLMs easier and opens options for me to use open source models on my own machine, putting my RTX 4090 to better use.</p>
<p>A bad programmer is someone who says they had trouble with ChatGPT and then spits out a canned response like &ldquo;have you tried Claude, have you tried Manus&rdquo; or &ldquo;ah, if you had used Cursor it would be better&rdquo;. Stop being that NPC, it&rsquo;s exhausting and demonstrates absolute LAZINESS.</p>
<p>A GOOD programmer is the one who goes through a journey like the one I just described in this post: learns the strong points, the weak points, how to CONTROL, how to SUBVERT, how to create CONTINGENCIES and, in the end, walks out with a BETTER solution than everyone else&rsquo;s.</p>
<p>That&rsquo;s what I became a programmer for, not to be a free billboard repeating Sam Altman&rsquo;s propaganda.</p>
]]></content:encoded><category>coding-agents</category><category>local-models</category><category>llms</category></item><item><title>LLM Hello World: Building Your Own Local AI Chat</title><link>https://akitaonrails.github.io/en/2025/04/25/llm-hello-world-building-your-own-local-ai-chat/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2025/04/25/llm-hello-world-building-your-own-local-ai-chat/</guid><pubDate>Fri, 25 Apr 2025 00:20:00 -0300</pubDate><description>&lt;p&gt;I&amp;rsquo;ve been playing A LOT with the various GPT derivatives like ChatGPT itself, Claude Sonnet, Gemini, and maybe later I&amp;rsquo;ll do another post about my experience. But today I wanted to dig a bit deeper and do a &amp;ldquo;Hello World&amp;rdquo; level experiment so that you, programmers, can understand how it works under the hood.&lt;/p&gt;
&lt;p&gt;I pushed the project to GitHub, it&amp;rsquo;s called &lt;a href="https://github.com/akitaonrails/Tiny-Qwen-CLI"target="_blank" rel="noopener"&gt;Tiny Qwen CLI&lt;/a&gt; and it was made strictly for EDUCATIONAL PURPOSES. For a real tool, look up &lt;a href="https://aider.chat/"target="_blank" rel="noopener"&gt;Aider&lt;/a&gt;. Maybe I&amp;rsquo;ll talk about it in another post later.&lt;/p&gt;</description><content:encoded><![CDATA[<p>I&rsquo;ve been playing A LOT with the various GPT derivatives like ChatGPT itself, Claude Sonnet, Gemini, and maybe later I&rsquo;ll do another post about my experience. But today I wanted to dig a bit deeper and do a &ldquo;Hello World&rdquo; level experiment so that you, programmers, can understand how it works under the hood.</p>
<p>I pushed the project to GitHub, it&rsquo;s called <a href="https://github.com/akitaonrails/Tiny-Qwen-CLI"target="_blank" rel="noopener">Tiny Qwen CLI</a> and it was made strictly for EDUCATIONAL PURPOSES. For a real tool, look up <a href="https://aider.chat/"target="_blank" rel="noopener">Aider</a>. Maybe I&rsquo;ll talk about it in another post later.</p>
<p>The concept I want to teach: GPTs are not magic.</p>
<h3>Concepts<span class="hx:absolute hx:-mt-20" id="concepts"></span>
    <a href="#concepts" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Watch my <a href="https://www.youtube.com/watch?v=UDrDg6uUOVs&amp;list=PLdsnXVqbHDUeowsAO0sChHDY4D65T5s1U&amp;pp=gAQB"target="_blank" rel="noopener">AI playlist</a> to understand how a GPT works. Once you&rsquo;ve watched it, let&rsquo;s dig in.</p>
<p>Underneath every Gemini, ChatGPT, Claude or any open one like Meta&rsquo;s LLAMA, there is a huge binary file that is the result of hours and hours of training with petabytes of data.</p>
<p>Training consists - VERY ROUGHLY - of tokenizing the words from texts (it&rsquo;s not exactly that, but think of breaking into syllables, the correct term is n-grams). Then transforming them into vectors (as you learned in college, a list of coordinates, usually <strong>FP</strong> or floating point, FP-32, 32-bits to be more precise). The training consists of moving these vectors around inside a hyper-dimensional space (way more than the 3 dimensions we&rsquo;re used to, think thousands or millions).</p>
<p>This positioning of vectors keeps changing as we feed it more texts. It keeps &ldquo;finding relationships&rdquo; between different tokens. We reach a point where every possible word has been encoded as vectors and this positioning gives them &ldquo;meaning&rdquo;. That&rsquo;s where the famous example comes from: if we take the word &ldquo;KING&rdquo; and subtract &ldquo;MAN&rdquo; then add &ldquo;WOMAN&rdquo; we&rsquo;ll find the vector for &ldquo;QUEEN&rdquo;.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>KING - MAN &#43; WOMAN = QUEEN</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This is no accident, it&rsquo;s because training managed to &ldquo;position&rdquo; these vectors in such a way that we can do &ldquo;math&rdquo; with these vectors and find meaning.</p>
<p>I&rsquo;m being ABSURDLY rough here, keep in mind this is dozens of papers and I&rsquo;m summarizing it in half a dozen paragraphs. What matters is understanding that this costs A LOT OF TIME, because every new token entering training has to sweep through every other token already computed and recalculate a huge chunk of them, all the time!</p>
<p>Hours and hours, days and days later, processing terabytes and terabytes of text, eventually we end up with a huge file of vectors, a <strong>VSM</strong> or Vector Space Model.</p>
<p>If we go to a site that catalogs and stores pre-trained models, on <a href="https://huggingface.co/meta-llama/Meta-Llama-3-70B/tree/main"target="_blank" rel="noopener">Meta&rsquo;s LLAMA 3 70B page</a>, we see this:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/34kujh0nzamxau15gqarr0rhp7v7" alt="Huggingface Meta"  loading="lazy" /></p>
<p>We have the big file partitioned into 30 smaller files of around 4.5 GB each, totaling about <strong>135 GB</strong>, for a 70B file, meaning 70 billion parameters. Parameters are not &ldquo;neurons&rdquo; or anything like that. They are weights and biases of the computations, stored in the database - I explain this in my videos, go watch them.</p>
<p>There are bigger versions, a GPT o4 has 1.8 trillion parameters. These are impressive numbers to throw around, but in practice, much above 300B, for general use, it doesn&rsquo;t make <em>that</em> much difference as people think. Maybe for very specific questions. It means yes, there are many more stored relationships, but that doesn&rsquo;t necessarily translate into proportionally better answers. It may just end up being more verbose than normal. Again, I&rsquo;m being rough, just trying to lower expectations. Stop being impressed by big numbers if you don&rsquo;t know what they mean.</p>
<p>Many other things influence answer quality besides parameters: quality and quantity of training data, model architecture, training methodology, context window size, fine-tuning, safety alignments and safety training (which usually make answers worse in the name of &ldquo;safety&rdquo;).</p>
<p>Smaller models, with more efficient architectures, higher-quality training data, focused on <strong>specific tasks</strong> beat a giant model that tries to know everything (<em>Jack of all Trades, Master of None</em>).</p>
<p>Being a bigger file with more parameters only means one thing: it will be proportionally heavier to run and probably won&rsquo;t fit in memory all at once and will have to keep <em>switching</em> chunks (like <em>memory bank switching</em>, for programmers out there).</p>
<p>In this Hello World project, I chose - randomly - to use the <a href="https://github.com/QwenLM/Qwen2.5-Coder"target="_blank" rel="noopener"><strong>Qwen 2.5 Coder 70B</strong></a> model. These open models like Llama are usually available in various sizes. I chose the 70B because it fits in the 24GB of VRAM I have on my RTX 4090 GPU. If your GPU is smaller, pick smaller models like 0.5B, 3B, 14B, 32B, and always prefer specialized models (like this one which is &ldquo;Coder&rdquo;). And always read the project page, which usually has documented code examples and fine-tuning. There is no general-purpose code that works for everything, each model has different characteristics.</p>
<p>Qwen 2.5 comes split into 6 files of 5 GB each, so you need to download <strong>30GB</strong> if you want to play with the &ldquo;extra-large&rdquo; version.</p>
<p>The code for this is actually pretty simple, because the AI community has already packaged all the tools you need into easy-to-use libraries, usually in Python (Python libs that talk to lower-level C libs, really), like <code>transformers</code> which does the heavy lifting of loading these files into memory and managing them.</p>
<p>These files usually have extensions like &ldquo;.safetensors&rdquo; which is kind of generic, or &ldquo;.pt&rdquo; or &ldquo;.pth&rdquo; which is more specific to the PyTorch format, which is a Python neural network library made by Meta. It&rsquo;s a competitor to Google&rsquo;s Tensorflow.</p>
<p>Tensors is the generic name for vectors or matrices. A scalar number is an order-0 Tensor, a vector is an order-1 Tensor, a matrix is an order-2 Tensor and so on. We don&rsquo;t have words for orders above 2, so you can just say order-3 Tensor, order-4, and infinitely up to millions of orders in an LLM.</p>
<p>CPUs are generic hardware made to deal primarily with order-0 tensors: integer scalar numbers. Operations like addition, subtraction, multiplication and so on are from one integer to another integer. Real numbers (with decimals) are &ldquo;simulated&rdquo;, splitting off an integer called the &ldquo;exponent&rdquo; from another integer called the &ldquo;mantissa&rdquo; (what you call the &ldquo;decimal&rdquo;) separated by a dot, the &ldquo;floating point&rdquo;. But essentially we are computing integers and truncating precision. More bits, more precision.</p>
<p>GPUs are hardware specialized in computing Tensors, in particular matrices. They were originally made to recompute effects on video images, like a video game in motion. Frames per second. A frame is made up of rows and columns of pixels: an order-2 tensor.</p>
<p>Instead of running a &ldquo;for&rdquo; loop and computing pixel by pixel, it&rsquo;s more efficient to pass an entire matrix to multiply with another matrix, a &ldquo;kernel&rdquo;, to obtain some effect like shadow, light, color change, distortions and much more. Programs that do this we usually call <strong>shaders</strong>.</p>
<p>A PyTorch or Tensorflow connects to a GPU using a library that gives them access to APIs so that, instead of passing images, they pass numeric matrices. NVIDIA has CUDA, AMD has ROCM, Apple has Metal, there&rsquo;s the general Vulkan project that tries to talk to all of them. But the most advanced and most used is still CUDA, because NVIDIA did get ahead years ago evangelizing that GPUs could be used for more than image shaders.</p>
<p>All that said, files like those &ldquo;.safetensors&rdquo; are loaded by the <code>transformers</code> library, which calls <code>torch</code> and tells CUDA to load them into the GPU&rsquo;s VRAM, where it can process them. The most basic code for this looks something like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">transformers</span> <span class="kn">import</span> <span class="n">AutoTokenizer</span><span class="p">,</span> <span class="n">AutoModelForCausalLM</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">device</span> <span class="o">=</span> <span class="s2">&#34;cuda&#34;</span> <span class="c1"># the device to load the model onto</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Now you do not need to add &#34;trust_remote_code=True&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">TOKENIZER</span> <span class="o">=</span> <span class="n">AutoTokenizer</span><span class="o">.</span><span class="n">from_pretrained</span><span class="p">(</span><span class="s2">&#34;Qwen/Qwen2.5-Coder-32B&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">MODEL</span> <span class="o">=</span> <span class="n">AutoModelForCausalLM</span><span class="o">.</span><span class="n">from_pretrained</span><span class="p">(</span><span class="s2">&#34;Qwen/Qwen2.5-Coder-32B&#34;</span><span class="p">,</span> <span class="n">device_map</span><span class="o">=</span><span class="s2">&#34;auto&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">eval</span><span class="p">()</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>We create a Tokenizer and a Model. The Tokenizer will &ldquo;tokenize&rdquo; whatever you ask in the chat and every text that goes into the session&rsquo;s context. This tokenization is &ldquo;mapped&rdquo; against the loaded model to generate an &ldquo;embedding&rdquo;, which is the vector representation of your text, normalized (compatible) for the loaded model. Different models convert your text into different embeddings. An embedding created with Qwen does not work directly with Llama.</p>
<p>Once you have these two objects, we can already do a chat:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># tokenize the input into tokens</span>
</span></span><span class="line"><span class="cl"><span class="n">input_text</span> <span class="o">=</span> <span class="s2">&#34;#write a quick sort algorithm&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">model_inputs</span> <span class="o">=</span> <span class="n">TOKENIZER</span><span class="p">([</span><span class="n">input_text</span><span class="p">],</span> <span class="n">return_tensors</span><span class="o">=</span><span class="s2">&#34;pt&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">to</span><span class="p">(</span><span class="n">device</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Use `max_new_tokens` to control the maximum output length.</span>
</span></span><span class="line"><span class="cl"><span class="n">generated_ids</span> <span class="o">=</span> <span class="n">MODEL</span><span class="o">.</span><span class="n">generate</span><span class="p">(</span><span class="n">model_inputs</span><span class="o">.</span><span class="n">input_ids</span><span class="p">,</span> <span class="n">max_new_tokens</span><span class="o">=</span><span class="mi">512</span><span class="p">,</span> <span class="n">do_sample</span><span class="o">=</span><span class="kc">False</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="c1"># The generated_ids include prompt_ids, so we only need to decode the tokens after prompt_ids.</span>
</span></span><span class="line"><span class="cl"><span class="n">output_text</span> <span class="o">=</span> <span class="n">TOKENIZER</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="n">generated_ids</span><span class="p">[</span><span class="nb">len</span><span class="p">(</span><span class="n">model_inputs</span><span class="o">.</span><span class="n">input_ids</span><span class="p">[</span><span class="mi">0</span><span class="p">]):],</span> <span class="n">skip_special_tokens</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Prompt: </span><span class="si">{</span><span class="n">input_text</span><span class="si">}</span><span class="se">\n\n</span><span class="s2">Generated text: </span><span class="si">{</span><span class="n">output_text</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>With <code>input_text</code> (what you typed in the chat) tokenized and converted, we can ask the model to <strong>COMPLETE</strong> this text, and it starts generating a new token after another, using that ATTENTION architecture, finding the best next word it can. At some point it finishes and returns it in <code>output_text</code> and that&rsquo;s it, that&rsquo;s your &ldquo;answer&rdquo;. As I always say, that&rsquo;s what GPT is: a TEXT GENERATOR.</p>
<h3>Tokens and Context Windows<span class="hx:absolute hx:-mt-20" id="tokens-and-context-windows"></span>
    <a href="#tokens-and-context-windows" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>One of the most annoying things about paid LLM services like ChatGPT or Claude is the limit on how many tokens we can use. They run out really fast if you&rsquo;re actually working every day. And they&rsquo;re not exactly cheap just for playing around. Yes, yes, I know, it depends on the service. I can use Cursor and get cheaper Claude credits than using Claude directly. I&rsquo;ll also get better answers using the API directly instead of the web chat, etc.</p>
<p>That&rsquo;s a conversation for another day, but it&rsquo;s my motivation to always have an open source option available and to know how to &ldquo;tune&rdquo; it for myself. Qwen 2.5 may not be better than Claude or Cursor - I&rsquo;ll explain why. But it&rsquo;s &ldquo;free&rdquo; and runs on my local machine (zero privacy concerns).</p>
<p>Check out my little &ldquo;hello world&rdquo; project working:</p>
<div class="video-container">
    <video controls>
        <source src="https://akitaonrails-videos.s3.us-east-2.amazonaws.com/qwen_cli2.mp4">
        Your browser does not support the video tag. [Direct Link](https://akitaonrails-videos.s3.us-east-2.amazonaws.com/qwen_cli2.mp4)
    </video>
</div>
<p>Note that it doesn&rsquo;t just give answers. At one point I ask it to read a local file of mine, and it can pull the code and analyze it. I&rsquo;ll explain how to do this shortly.</p>
<p>The point now is that it has enough context window to load long stuff like source code files. I haven&rsquo;t tested anything very heavy, but this 70B model easily takes up nearly 20GB on my GPU:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/ykbdo2ekv12woxbx964um0osphk0" alt="nvidia-smi"  loading="lazy" /></p>
<p>To configure it, I made a config block like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">DEFAULT_CONFIG</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;model_repo&#34;</span><span class="p">:</span> <span class="s2">&#34;Qwen/Qwen2.5-Coder-14B-Instruct&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;model_dir&#34;</span><span class="p">:</span> <span class="nb">str</span><span class="p">(</span><span class="n">Path</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">environ</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;MODELS_DIR&#34;</span><span class="p">,</span> <span class="s2">&#34;/models&#34;</span><span class="p">))</span> <span class="o">/</span> <span class="s2">&#34;Qwen2.5-Coder-14B-Instruct&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;quantization&#34;</span><span class="p">:</span> <span class="s2">&#34;8bit&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;max_context_tokens&#34;</span><span class="p">:</span> <span class="mi">120000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;max_new_tokens&#34;</span><span class="p">:</span> <span class="mi">10000</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;temperature&#34;</span><span class="p">:</span> <span class="mf">0.1</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;model_download_timeout&#34;</span><span class="p">:</span> <span class="mi">1800</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;helpers_dir&#34;</span><span class="p">:</span> <span class="s2">&#34;helper_functions&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>What matters, first, is that I&rsquo;m choosing to run computations with FP8 (floating-point 8-bits), even though I think this model is FP-16. If you leave it at 16-bits, answers become much slower, at a level you can feel. Like, one word per second. I understood that the one doing this on-the-fly conversion is the Python <code>bitsandbytes</code> library, and you need to have it installed.</p>
<p>Another important part are the logs I leave visible when loading the model, you&rsquo;ll see the following if you try to run it:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>Qwen attention is NOT SDPA-compatible, or SDPA is not available. Trying xFormers...
xFormers is available. Enabling it for attention.
Sliding Window Attention is enabled but not implemented for `sdpa`; unexpected results may be encountered.
2025-04-25 02:34:27,638 [INFO] accelerate.utils.modeling: We will use 90% of the memory on device 0 for storing the model, and 10% for the buffer to avoid OOM. You can set `max_memory` in to a higher value to use more memory (at your own risk).
Loading checkpoint shards: 100%|██████████████████████████████████████████████████████████| 6/6 [00:07&lt;00:00,  1.29s/it]
SDPA is available and (hopefully) being used!</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><code>transformers</code> warns that &ldquo;Sliding Window Attention is enabled but doesn&rsquo;t implement SDPA&rdquo;. And I do a trick with <code>xformers</code> to try to enable it (in the end it says &ldquo;SDPA is available&rdquo;).</p>
<p>What is <strong>SLIDING WINDOW ATTENTION</strong>? Roughly speaking, the whole idea of this generation of GPT is the concept of ATTENTION, from the paper <a href="https://arxiv.org/abs/1706.03762"target="_blank" rel="noopener">&ldquo;Attention is all you Need&rdquo;</a>, which was the kickoff for everything we have today related to GPT and LLM.</p>
<p>The important concept is that the answers seem so &ldquo;good&rdquo; because it uses this &ldquo;attention&rdquo; process to generate them.</p>
<p>In the beginning, in the GPT 2 era, attention was very &ldquo;focused&rdquo; on a small context. Around 1024 tokens, which is almost nothing. You can ask short questions, but source code will never fit.</p>
<p>Then we kept evolving, you&rsquo;ve seen Gemini which claims it handles 1 million tokens. Even this Qwen 2.5 claims to handle 1 million tokens too. Roughly think of each token as 4 letters, on average.</p>
<p>A source code file, on average, has lines of 40 characters (some longer, some shorter). So that&rsquo;s about <strong>10 tokens per line</strong>. An average file has around 500 lines x 10 tokens/line, which already gives around 5 thousand tokens.</p>
<p>An entire small project, with about 20 files x 5,000 tokens, is already around 100,000 tokens. Sure, it&rsquo;s an average. But notice that with less than 100 thousand tokens you can&rsquo;t have a whole project in context.</p>
<p><strong>&ldquo;Ah, so if I use Gemini or this Qwen, 200 files will fit??&rdquo;</strong></p>
<p>Sort of. Here&rsquo;s where Sliding Window comes in. Think that even with a huge context, <strong>there&rsquo;s no way to pay attention to everything at the same time</strong>, so it searches for what you&rsquo;re asking right now, and &ldquo;slides the attention window&rdquo; only to the part of the context that seems most relevant. That&rsquo;s why even when loading lots of material, it keeps it in memory, but <strong>only pays attention to one chunk at a time</strong>, that&rsquo;s SLIDING WINDOW ATTENTION.</p>
<p>To do this we have the &ldquo;SCALED DOT PRODUCT ATTENTION&rdquo; or SDPA feature, which is what <code>transformers</code> complained it might not have, which is one of the ways to do Sliding Window Attention.</p>
<p>In my code, to load the model and try to use SDPA I tried this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">xformers.ops</span>  <span class="c1"># Test if xFormers is installed</span>
</span></span><span class="line"><span class="cl"><span class="n">model_config</span><span class="o">.</span><span class="n">attention_implementation</span> <span class="o">=</span> <span class="s2">&#34;flash_attention_2&#34;</span>  <span class="c1"># Or &#34;memory_efficient&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="s2">&#34;xFormers is available. Enabling it for attention.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="nb">hasattr</span><span class="p">(</span><span class="n">torch</span><span class="o">.</span><span class="n">nn</span><span class="o">.</span><span class="n">functional</span><span class="p">,</span> <span class="s2">&#34;scaled_dot_product_attention&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;SDPA is available and (hopefully) being used!&#34;</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And that&rsquo;s why, in the end, it finishes with &ldquo;SDPA is available&rdquo;. It &ldquo;looks&rdquo; like it worked. If you want to test it, there&rsquo;s this little snippet you can pass in to see if it works:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">torch</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="ow">not</span> <span class="nb">hasattr</span><span class="p">(</span><span class="n">torch</span><span class="o">.</span><span class="n">nn</span><span class="o">.</span><span class="n">functional</span><span class="p">,</span> <span class="s2">&#34;scaled_dot_product_attention&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;SDPA not available&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">exit</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">q</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">randn</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">16</span><span class="p">)</span><span class="o">.</span><span class="n">to</span><span class="p">(</span><span class="s2">&#34;cuda&#34;</span> <span class="k">if</span> <span class="n">torch</span><span class="o">.</span><span class="n">cuda</span><span class="o">.</span><span class="n">is_available</span><span class="p">()</span> <span class="k">else</span> <span class="s2">&#34;cpu&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">k</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">randn</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">16</span><span class="p">)</span><span class="o">.</span><span class="n">to</span><span class="p">(</span><span class="n">q</span><span class="o">.</span><span class="n">device</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">v</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">randn</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">16</span><span class="p">)</span><span class="o">.</span><span class="n">to</span><span class="p">(</span><span class="n">q</span><span class="o">.</span><span class="n">device</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">output</span> <span class="o">=</span> <span class="n">torch</span><span class="o">.</span><span class="n">nn</span><span class="o">.</span><span class="n">functional</span><span class="o">.</span><span class="n">scaled_dot_product_attention</span><span class="p">(</span><span class="n">q</span><span class="p">,</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;SDPA works in isolation!&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;SDPA fails in isolation: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Dot Product, or matrix scalar product, is one of the most used operations in LLM models. I understood that without SDPA, you&rsquo;ll still have Sliding Window, but with SDPA you get better attention results. How much better? Hard to quantify, I just understood it&rsquo;s better.</p>
<p>But this is one of the techniques behind why nowadays you can have huge contexts of 1 million tokens: because you don&rsquo;t need to pay attention to everything at the same time, but rather windows at different positions for each new question. Get the trick?</p>
<p>And with that, in my config I can try to increase <code>max_context_tokens</code> (which I left at only 120 thousand tokens) and <code>max_new_tokens</code> which is the per-answer token limit (which I left at 10 thousand tokens). Try bumping these numbers up to a max of 1 million to see how it goes.</p>
<p><strong>Context only matters within the sliding context window. It&rsquo;s good to pass plenty of context. But understand that it won&rsquo;t pay attention to everything at the same time, but within a sliding window.</strong></p>
<p>Another important parameter is <code>temperature</code>, which I left low at 0.1. The higher it is, the more &ldquo;creative&rdquo; it will be in its answers. The lower it is, the more &ldquo;exact&rdquo; it tends to be. It&rsquo;s another parameter worth exploring.</p>
<h3>Prompts and &ldquo;Agents&rdquo;<span class="hx:absolute hx:-mt-20" id="prompts-and-agents"></span>
    <a href="#prompts-and-agents" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The best way to get better answers is to start every new session, whether via API or via the web chat, describing a &ldquo;persona&rdquo;, how you want the GPT to answer. The more descriptive you are, the better. For example, I have this snippet in my program:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">build_system_prompt</span><span class="p">(</span><span class="n">tool_prompts</span><span class="p">:</span> <span class="n">List</span><span class="p">[</span><span class="nb">str</span><span class="p">])</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">base</span> <span class="o">=</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;You are Qwen2.5 Coder, a highly skilled AI assistant specializing in software development.</span><span class="se">\n</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;Your capabilities include code analysis, explanation, error detection, and suggesting improvements.</span><span class="se">\n</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">tools_section</span> <span class="o">=</span> <span class="s2">&#34;TOOLS:</span><span class="se">\n</span><span class="s2">&#34;</span> <span class="o">+</span> <span class="s2">&#34;</span><span class="se">\n</span><span class="s2">&#34;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">tool_prompts</span><span class="p">)</span> <span class="o">+</span> <span class="s2">&#34;</span><span class="se">\n\n</span><span class="s2">&#34;</span> <span class="k">if</span> <span class="n">tool_prompts</span> <span class="k">else</span> <span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">rules_section</span> <span class="o">=</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;IMPORTANT RULES:</span><span class="se">\n</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;1. You MUST use the appropriate tool when necessary.</span><span class="se">\n</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;2. You MUST NOT reveal the tool commands to the user.</span><span class="se">\n</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;3. After a tool is used, continue the conversation as if you have direct access to the content.</span><span class="se">\n</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;4. If a file fails to load, inform the user clearly.</span><span class="se">\n</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;5. Do NOT ask for file/URL content directly; use tools.</span><span class="se">\n</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;6. Once you&#39;ve executed [LOAD_FILE ...], you MUST immediately use the loaded content. &#34;</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;Never say you cannot read it — if you see [LOAD_FILE &lt;path&gt;] then you now *have* it.</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">base</span> <span class="o">+</span> <span class="n">tools_section</span> <span class="o">+</span> <span class="n">rules_section</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This is a fairly short example but I&rsquo;m already setting what kind of conversation I want to have with it and what rules I want it to follow. Not always, but most of the time, it tends to follow. Remember: the longer the chat gets, the bigger the context, the more the window slides, and the further away these rules get, until a point comes where it starts to &ldquo;forget&rdquo; to act the way I asked.</p>
<p>Understanding this, there&rsquo;s another trick I added. In this project there&rsquo;s a directory called <a href="https://github.com/akitaonrails/Tiny-Qwen-CLI/"target="_blank" rel="noopener">helper_functions</a> where we have scripts like <code>load_file.py</code> or <code>fetch_url.py</code>. In the earlier code note how it concatenates something in the middle of the prompt, called &ldquo;TOOLS&rdquo;. When the chat starts, I print to the screen and you&rsquo;ll see this snippet:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>...
TOOLS:
[BATCH_LOAD args] – If the user asks to read, load or analyze all the files from a relative path, such as ./src or similar,
[LOAD_FILE args] – Whenever the user asks to read, load, analyze some code and provides a relative path, such as ./file.py or utils/utils.py or similar,
[FETCH_URL args] – Whenever the user asks to read, load, research or consult a URL,
...</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>So, if at some point, I ask:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/hiz90dk6owf2otjfixiftjxavm4n" alt="qwen_cli fetch_url"  loading="lazy" /></p>
<p>this &ldquo;can you read a website at https://www&hellip;.&rdquo; will make the model follow the initial prompt rule and type into the chat:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>[FETCH_URL https://www...]</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>My chat program has a &ldquo;parser&rdquo; that monitors the history for these special keywords:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">parse_special_commands</span><span class="p">(</span><span class="n">response</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">List</span><span class="p">[</span><span class="nb">tuple</span><span class="p">]:</span>
</span></span><span class="line"><span class="cl">    <span class="n">pattern</span> <span class="o">=</span> <span class="sa">r</span><span class="s1">&#39;\[([A-Z_]+)\s+([^\]]+)\]&#39;</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">[(</span><span class="n">m</span><span class="o">.</span><span class="n">group</span><span class="p">(</span><span class="mi">1</span><span class="p">),</span> <span class="n">m</span><span class="o">.</span><span class="n">group</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span><span class="o">.</span><span class="n">strip</span><span class="p">(),</span> <span class="n">m</span><span class="o">.</span><span class="n">start</span><span class="p">(),</span> <span class="n">m</span><span class="o">.</span><span class="n">end</span><span class="p">())</span> <span class="k">for</span> <span class="n">m</span> <span class="ow">in</span> <span class="n">re</span><span class="o">.</span><span class="n">finditer</span><span class="p">(</span><span class="n">pattern</span><span class="p">,</span> <span class="n">response</span><span class="p">)]</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>It&rsquo;s a pretty primitive parser, using REGEX to grab these snippets. This is where I say my project is educational. I imagine a Co-pilot, Cursor, etc, has a much more robust parser. I explained how to build parsers with ANTLR on my channel, remember? One of the videos is <a href="https://www.youtube.com/watch?v=_7nISfpofec&amp;pp=ygUTYWtpdGFvbnJhaWxzIHBhcnNlcg%3D%3D"target="_blank" rel="noopener">&ldquo;Eu fiz um servidor de SQL?&rdquo;</a>. Real parsers aren&rsquo;t built with REGEX, but for proofs of concept, it does the job.</p>
<p>Then in the chat loop there&rsquo;s this snippet:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">response_text</span> <span class="o">=</span> <span class="n">QwenSession</span><span class="o">.</span><span class="n">_tokenizer</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="n">generated</span><span class="p">,</span> <span class="n">skip_special_tokens</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Other special commands</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">cmd_type</span><span class="p">,</span> <span class="n">cmd_arg</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="n">_</span> <span class="ow">in</span> <span class="n">parse_special_commands</span><span class="p">(</span><span class="n">response_text</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">cmd_type</span> <span class="ow">in</span> <span class="n">helper_functions</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">result</span> <span class="o">=</span> <span class="n">helper_functions</span><span class="p">[</span><span class="n">cmd_type</span><span class="p">](</span><span class="n">cmd_arg</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="n">result</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">history</span><span class="o">.</span><span class="n">append</span><span class="p">({</span><span class="s2">&#34;role&#34;</span><span class="p">:</span> <span class="s2">&#34;system&#34;</span><span class="p">,</span> <span class="s2">&#34;content&#34;</span><span class="p">:</span> <span class="n">result</span><span class="p">})</span>
</span></span><span class="line"><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">history</span><span class="o">.</span><span class="n">append</span><span class="p">({</span><span class="s2">&#34;role&#34;</span><span class="p">:</span> <span class="s2">&#34;user&#34;</span><span class="p">,</span> <span class="s2">&#34;content&#34;</span><span class="p">:</span> <span class="s2">&#34;Please continue the analysis using the loaded file.&#34;</span><span class="p">})</span>
</span></span><span class="line"><span class="cl">            <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;✅ [</span><span class="si">{</span><span class="n">cmd_type</span><span class="si">}</span><span class="s2">] processed &#39;</span><span class="si">{</span><span class="n">cmd_arg</span><span class="si">}</span><span class="s2">&#39;&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">chat</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">                <span class="n">prompt</span><span class="p">,</span> <span class="n">helper_functions</span><span class="p">,</span> <span class="n">max_new_tokens</span><span class="p">,</span> <span class="n">temperature</span><span class="p">,</span> <span class="n">stream</span><span class="p">,</span> <span class="n">hide_reasoning</span>
</span></span><span class="line"><span class="cl">            <span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>It decodes the generated tokens, passes them to the parser, and if there&rsquo;s a special command, it calls <code>helper_functions</code> which dynamically calls <code>fetch_url.py</code> which is a dumb script that will use <code>urllib</code> to load the page.</p>
<p>With the content loaded, it concatenates it back into <code>self.history</code> which is the chat history, using <code>.append</code>, and sends a message - as if it were me, the &ldquo;user&rdquo; - asking the model to continue, now that it has the page&rsquo;s content.</p>
<p>And as you can see in the screenshot above, it can give me the summary of the GitHub page I passed. That&rsquo;s how the various GPTs manage to pull more current information from sites that didn&rsquo;t exist at the time the pre-trained model was trained.</p>
<p>Another example, I can ask it to load a code file of mine and ask it to analyze, or fix bugs, or refactor it however I want:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/lt891apoaebrmx0y4k7ngtk2l5ol" alt="qwen_cli load_file"  loading="lazy" /></p>
<p>It&rsquo;s similar to the &ldquo;file upload&rdquo; feature in ChatGPT. Again:</p>
<ul>
<li>the model will follow the initial prompt rules and will type the <code>[LOAD_FILE ...]</code> command</li>
<li>my <code>parser_special_commands</code>, with its REGEX, will find this command and split the arguments</li>
<li>the chat loop will call <code>helper_functions</code> and map to the <code>load_file.py</code> script, which is a dumb script that reads files</li>
<li>the file content is concatenated back into the context <code>history</code> and the model can now keep responding based on that content.</li>
</ul>
<p>Understand, those &ldquo;.safetensors&rdquo; files are <strong>READ-ONLY</strong>. Nothing is written there. Doesn&rsquo;t matter which one. Every LLM is <strong>CLOSED FOR WRITING</strong>. Nothing you write in the chat is recorded in the model.</p>
<p>Remember how I said the number of &ldquo;parameters&rdquo; doesn&rsquo;t determine the quality of the model? What <strong>MOST</strong> determines quality is the <strong>QUALITY OF THE TRAINING MATERIAL</strong>. And chats, to a certain extent, are useful as samples of how the model should answer, but 90% or more I claim is pure and total <strong>GARBAGE</strong>. Every chat we write is garbage and no good for training, it would just WORSEN the final model and make it heavier with useless information.</p>
<p>Write all the &ldquo;Thank You Very Much&rdquo; or &ldquo;Please&rdquo; you want. It&rsquo;s all discarded at the end, nothing is saved, and no ChatGPT, Gemini, Claude, Deepseek, Qwen or Llama will ever know anything about what you said to it. It&rsquo;s all totally <strong>SEPARATE AND DISPOSABLE</strong> information.</p>
<p>Everything I typed in those screenshot examples as my little Qwen chat gets discarded the moment I type &ldquo;bye&rdquo; or hit &ldquo;Ctrl+D&rdquo; to exit. That&rsquo;s what we call a &ldquo;session&rdquo; or &ldquo;history&rdquo;. It&rsquo;s just a useless text file as far as the model is concerned. It only serves us, humans, as notes to use later, if you want. My little program doesn&rsquo;t even have a feature to save this history. When you exit, everything is clean. So, feel free to curse away in my chat.</p>
<p>There are two important points in this section:</p>
<ul>
<li>the better the rules you describe at the start, the better the answer results tend to be. This applies to any LLM.</li>
<li>you can &ldquo;agree&rdquo; with the model to spit out &ldquo;pseudo-commands&rdquo; and have an external program that does the parsing and actually executes the commands, like I did with my &ldquo;helpers&rdquo;. That&rsquo;s what we call agents. An LLM agent is a little program that returns some content back to the session&rsquo;s context history.</li>
</ul>
<p>Where are Cursor or Co-pilot better than this educational little program of mine? They come pre-configured with DOZENS of pre-made rules they&rsquo;ve tested extensively to get better results, and they ship with a library of DOZENS of small scripts - like my helpers - to pull information from your project or your IDE to improve context.</p>
<p>But the secret is this: <strong>RULE PROMPTS</strong> and <strong>CONTEXT SCRIPTS</strong>.</p>
<p>If I were to summarize the entire Agents or MCPs ecosystem or whatever it&rsquo;s called these days, it&rsquo;s this combination of factors to improve answers.</p>
<p>Another trick is using MORE THAN ONE LLM. Remember I said specialized LLMs are better? Besides Qwen 2.5 Coder, which people say is good for code, there was already WizardCoder before it, Code Llama, StarCoder, now there&rsquo;s Deepseek Coder and several others, some even specialized in specific languages. You can experiment to see which gives better results for your case.</p>
<p>If you save the session as text, you can unload one LLM, load another LLM, re-tokenize and re-load it into the context of this other LLM and try a different result - if anyone wants to implement this feature in my little project, Pull Requests are welcome.</p>
<p>As I said at the beginning, that&rsquo;s what software like <a href="https://aider.chat/"target="_blank" rel="noopener">Aider</a> does. I haven&rsquo;t played with it enough yet, but it connects to ChatGPT, Gemini, Claude and any other, and you can enable as many as you want. I think it also supports local models like Qwen, so there are already tools that do this. And that&rsquo;s the third leg for better answers: MULTI-LLM.</p>
<h3>Next Steps<span class="hx:absolute hx:-mt-20" id="next-steps"></span>
    <a href="#next-steps" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Instead of just being impressed by the pile of tools out there, understand these fundamental pieces. Knowing how things work always gives you more options to forge alternatives that work better for specific cases. You, who claim to be a programmer, should be exploring these options - and not just settling for the limitations and waiting for someone to solve your problem. Roll up your sleeves and solve it yourself. Isn&rsquo;t that why you became a programmer???</p>
]]></content:encoded><category>llms</category><category>local-models</category><category>coding-agents</category></item><item><title>Changing Clothes Using A.I. (ComfyUI)</title><link>https://akitaonrails.github.io/en/2025/04/23/changing-clothes-using-ai-comfyui/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2025/04/23/changing-clothes-using-ai-comfyui/</guid><pubDate>Wed, 23 Apr 2025 17:45:00 -0300</pubDate><description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt; this post is now obsolete. I now use &lt;a href="https://github.com/akitaonrails/aitrepreneur-docker"target="_blank" rel="noopener"&gt;&lt;strong&gt;akitaonrails/aitrepreneur-docker&lt;/strong&gt;&lt;/a&gt;, which runs Aitrepreneur&amp;rsquo;s stacks locally in Docker with pre-loaded models and workflows. Check the README for details.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In the &lt;a href="https://www.akitaonrails.com/en/2025/04/23/using-ai-comfyui-to-generate-npcs-for-game-development/"target="_blank" rel="noopener"&gt;previous post&lt;/a&gt; I showed how to generate character sheets for game development. But some folks asked me &amp;ldquo;what if I want specific clothes?&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Hold my Beer&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a way. It&amp;rsquo;s a separate workflow that you can later add to the previous workflow if you want (homework assignment). Just take the character generated in the first group of nodes and pass it through this other workflow that swaps clothes. (Or do the extra work of generating loras and activating them with keywords in the prompt as well, there isn&amp;rsquo;t only one way.)&lt;/p&gt;</description><content:encoded><![CDATA[<blockquote>
  <p><strong>Update:</strong> this post is now obsolete. I now use <a href="https://github.com/akitaonrails/aitrepreneur-docker"target="_blank" rel="noopener"><strong>akitaonrails/aitrepreneur-docker</strong></a>, which runs Aitrepreneur&rsquo;s stacks locally in Docker with pre-loaded models and workflows. Check the README for details.</p>

</blockquote>
<p>In the <a href="https://www.akitaonrails.com/en/2025/04/23/using-ai-comfyui-to-generate-npcs-for-game-development/"target="_blank" rel="noopener">previous post</a> I showed how to generate character sheets for game development. But some folks asked me &ldquo;what if I want specific clothes?&rdquo;.</p>
<p><strong>Hold my Beer</strong></p>
<p>There&rsquo;s a way. It&rsquo;s a separate workflow that you can later add to the previous workflow if you want (homework assignment). Just take the character generated in the first group of nodes and pass it through this other workflow that swaps clothes. (Or do the extra work of generating loras and activating them with keywords in the prompt as well, there isn&rsquo;t only one way.)</p>
<p>Unlike Mick&rsquo;s workflow - which is paid - this other one is open. I grabbed it from some Reddit post and dropped it into the workflows folder of my Docker project as &ldquo;garmente-replacement-idm-vton.json&rdquo;. Just load it in ComfyUI and play around. Let&rsquo;s see the result. Here are two images. The result isn&rsquo;t perfect, but see if you can figure out which of the two is the original and which one was generated by A.I.:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/6b56mdoiwf82xdh7cdhdc7tg0ic2" alt="Hemsworth 1"  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/w0s9nthw6jt86en8wl09m67ryzpc" alt="Hemsworth 2"  loading="lazy" /></p>
<p>Answer at the end of the post lol</p>
<p>Anyway, the complete workflow looks like this:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/xbgu7dzrjzflhfsld6jtrl5jc3ia" alt="workflow"  loading="lazy" /></p>
<p>In the current state of my setup there&rsquo;s a glitch I haven&rsquo;t been able to solve. I think it&rsquo;s the JNodes extension that&rsquo;s opening an infernal popup at the top left of the interface, which won&rsquo;t close no matter what. For now I&rsquo;m opening the Inspector and deleting the Node from the HTML directly lol it&rsquo;s a hack but it works:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/p8q4hmwjmi4opsu8u045jxrpma2v" alt="JNode bug"  loading="lazy" /></p>
<p>If anyone knows how to fix it, I&rsquo;d really appreciate it. There&rsquo;s an alert from an extension that seems to be outdated, this front-end something, but just ignore it. Of course it had to be front-end-something&hellip;</p>
<p>Anyway. In this first group it&rsquo;s simple, just load an image as clean and clear as possible of the new garment you want to use. And then the original photo where you want this clothing to be applied. It doesn&rsquo;t perform miracles. If the photo is of someone in a blazer and you want to change it to a tank top, sometimes it manages, but don&rsquo;t expect the arms and torso to look good. It&rsquo;s going to redraw everything as it sees fit, or it&rsquo;ll end up putting the tank top on top of the blazer. Anyway, it&rsquo;s A.I., the clearer the goal, the better the result. If the new garment is a blazer, ideally the original was another blazer, it&rsquo;ll fit better.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2owpldebsc91y6vfsayscdkuqtui" alt="Load Images"  loading="lazy" /></p>
<p>Heads up that the IDM-VTON models really weigh a <strong>TON</strong>. It alone downloads no less than <strong>30GB</strong> of models (already included in my setup) and will demand more than 20GB of VRAM, so anything below an RTX 3090 with 24GB just won&rsquo;t run. You can run it online, on the Hugging Face site. Watch this tutorial that shows how:</p>
<!-- youtube video unavailable (deleted/blocked): WL59FqL0L-s -->
<p>This is the <strong>Aiconomist</strong> channel. He shows how to run it online and how to set it up on your machine in <a href="https://youtu.be/pFYqPf1Z7x8?si=Ryu5vxPmKGr4zhWx"target="_blank" rel="noopener">this other video</a> which I used as reference to build my workflow. Subscribe to his channel, it&rsquo;s worth it. The tutorials are very well explained and have more details that I won&rsquo;t cover in this post.</p>
<p>That said, there&rsquo;s a bug I haven&rsquo;t solved yet. After running the workflow, the model stays hanging in the GPU&rsquo;s VRAM and if I try to run again, it FREEZES. Then I have to shut down the Docker container and restart it to recover the memory. I tried a Clear VRAM GPU node but it didn&rsquo;t work. If anyone knows how to fix it, I&rsquo;d appreciate that too.</p>
<p>The next step is recurring across many workflows: extracting layers of information from the original photo. Getting a mask of the garment we want to replace and information about the original pose:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/kgs8jj5m7cgt2nx4kv8yazfpbfg5" alt="mask and pose"  loading="lazy" /></p>
<p>This is why ComfyUI workflows are powerful once you start understanding how the process is broken down. These parts are reusable and different models give different results depending on which controlnets you add.</p>
<p>Then we get to the main part: wiring all this information into the IDM-VTON node, which will reposition the new garment on top of the original photo:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/7xnxbrcy22trdctbf5a1dhh0rqjw" alt="IDM-VTON"  loading="lazy" /></p>
<p>Notice that the result isn&rsquo;t very good, it looks pretty &ldquo;fake&rdquo;. And there are parameters you can tweak to improve it. But the use case isn&rsquo;t to swap for a <strong>specific</strong> garment but rather for a <strong>similar</strong> one. I&rsquo;ll explain in a bit, but first, a tangent.</p>
<p>The repository for the original extension of this Node is <a href="https://github.com/TemryL/ComfyUI-IDM-VTON"target="_blank" rel="noopener">this one</a>. But if you try to install it, it breaks. It seems to be a bit out of date with ComfyUI and needs to run on an older version. But that&rsquo;s a pain. So I went ahead and made a <a href="https://github.com/akitaonrails/ComfyUI-IDM-VTON"target="_blank" rel="noopener"><strong>FORK</strong></a> with the necessary fixes to run on the newer version (the newer versions of the Python packages diffusers, transformers, huggingface_hub, break IDM-VTON).</p>
<p>By the way, Python dependency management is pretty BAD. But ComfyUI&rsquo;s is even worse, because different extensions (which install Python dependencies globally) can be at different stages of development. Every time ComfyUI itself changes, everyone needs to update fast. That&rsquo;s why the ideal is not to install the master branch version of ComfyUI, always a stable version (marked with a tag) or two versions back, just to be safe. But then there are new extensions that require you to be on the newest version. As I said, it&rsquo;s a nightmare to manage. Being a programmer, I can &ldquo;hack around&rdquo; when errors happen, but normal users will suffer a bit.</p>
<p>Rant over. In the previous step the first part of the flow ends. When it hits this Image Sender node, the process stops - I don&rsquo;t know why. And you have to click the &ldquo;RUN&rdquo; button a second time. Then the other node, Image Receiver, will receive the new image. I think I&rsquo;ll just delete those two nodes and connect the &ldquo;image&rdquo; output of the Run IDM-VTON Inference node directly to the next node, which are the Image Composite Masked and the VAE Encode.</p>
<p>Let&rsquo;s go back a step:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/jj950u7842rkc9lxniz3uoztbszv" alt="New photo, new garment"  loading="lazy" /></p>
<p>In this next step, on the second &ldquo;RUN&rdquo;, it takes the image with the new garment, but low quality, and loads a new checkpoint (in this case juggernaultXL but it could be any other like SDXL itself, FLUX or dreamsharper). The model will determine the new photo, which is what gets generated at the end, on the right.</p>
<p>Notice that the new photo has much better garment quality, but the face is completely different. It used the information from the original photo, kept the pose, the body proportions, even the background, but it can&rsquo;t preserve the face. No problem, because we can help.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/bw9sram1maspiob2ubo4r904oriy" alt="mix photos"  loading="lazy" /></p>
<p>Remember that in the previous step we got the mask (black and white) of the exact position of the garment? We can enlarge the mask and apply a Gaussian Blur (so it later blends nicely into the original photo) and use that to crop from the generated photo with the different face, using the <a href="https://github.com/tencent-ailab/IP-Adapter/"target="_blank" rel="noopener"><strong>IPAdapter</strong></a> model (which alone is another 30GB of models - I told you it&rsquo;s heavy).</p>
<p>If I understood correctly, IP Adapter is responsible for being able to generate new images that are COHERENT with each other, with the same face, or same clothes, but in other positions, for example, a kind of conditioned generation. With it you can control the coherence of new images A LOT, generate a character in different positions, in different backgrounds and always preserving the characteristics of the original character.</p>
<p>IP Adapter will use the original photo, keep the face, use the masks and blend with the photo with the different face, taking only the garment, and then assemble everything in the Image Composite Masked which is the final Node of the process.</p>
<p>In the end, we can compare the first attempt at putting on the new garment (which came out ok but ugly, and the new garment, which came out different but prettier) and everything integrated into the original photo, preserving the same face.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/uawsqhhr6d8kii8getzkq6ebxgh9" alt="image compare"  loading="lazy" /></p>
<p>As I said, the qualities are affected by the choice of model, the KSampler settings, number of steps, cfg and everything else. You have to experiment and generate several until you reach the result you like best. That&rsquo;s also how you learn more. You don&rsquo;t just click once and magically have the result. A good artist experiments dozens of times.</p>
<p>The final image might not be suitable for a Vogue cover (although I&rsquo;ve already seen plenty of official covers that were VERY questionable). It depends on the need. In particular it&rsquo;s good for prototyping. &ldquo;I wonder how this garment looks on my model?&rdquo; Instead of calling the model in, having her change clothes, or spending an hour Photoshopping, in minutes you can try several different garments and see whether the model fits the garment, without wasting time. So a photographer can just show it to the client before they spend money on studio, assistants, equipment, etc. You can visualize quickly and discuss details much faster.</p>
<p>This is the kind of thing where tools like this can help a lot and differentiate one artist from another, not by replacing the artist, but by creating more options. And the main thing: all these workflows are modifiable, you reprogram them and combine them with others however you want. The limit is your creativity.</p>
]]></content:encoded><category>generative-media</category><category>local-models</category></item><item><title>Using A.I. (ComfyUI) to Generate NPCs in Game Development</title><link>https://akitaonrails.github.io/en/2025/04/23/using-ai-comfyui-to-generate-npcs-for-game-development/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2025/04/23/using-ai-comfyui-to-generate-npcs-for-game-development/</guid><pubDate>Wed, 23 Apr 2025 16:25:00 -0300</pubDate><description>&lt;p&gt;I have been updating my little ComfyUI with Docker Compose project A LOT. I think it is the most complete and tested setup out there, with the advantage of being in Docker, so it is repeatable with zero setup. Read &lt;a href="https://github.com/akitaonrails/ComfyUI-Docker-CUDA-preloaded"target="_blank" rel="noopener"&gt;my post&lt;/a&gt; for more details on how it works.&lt;/p&gt;
&lt;p&gt;I decided to document in this post one of the most complex workflows I have tested that runs on my setup: how to develop Character Sheets of random characters, in any style, whether realistic, Pixar style, cartoon style, whatever you want. The result is a sheet like the example below. In several poses, several lighting setups, several emotional expressions, with a color index, all generated from absolute scratch by &amp;ldquo;A.I.&amp;rdquo; In half an hour of processing on my RTX 4090.&lt;/p&gt;</description><content:encoded><![CDATA[<p>I have been updating my little ComfyUI with Docker Compose project A LOT. I think it is the most complete and tested setup out there, with the advantage of being in Docker, so it is repeatable with zero setup. Read <a href="https://github.com/akitaonrails/ComfyUI-Docker-CUDA-preloaded"target="_blank" rel="noopener">my post</a> for more details on how it works.</p>
<p>I decided to document in this post one of the most complex workflows I have tested that runs on my setup: how to develop Character Sheets of random characters, in any style, whether realistic, Pixar style, cartoon style, whatever you want. The result is a sheet like the example below. In several poses, several lighting setups, several emotional expressions, with a color index, all generated from absolute scratch by &ldquo;A.I.&rdquo; In half an hour of processing on my RTX 4090.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/469eyg4a4pl3ry0z12twg74nlbc8" alt="NPC Design Sheet"  loading="lazy" /></p>
<p>Imagine you are an intern at a game studio. Your job: create 100 background characters (NPCs). In the end you have to deliver 100 of them. But just because they are secondary characters does not mean you can do them any old way, they all have to be coherent, in every pose. Imagine how many days you would take. What if you could automate it? This is the kind of scenario where A.I. is interesting. Spend more time on the main characters, do them manually, but do not waste time on the secondary ones and do not lower the final quality of everything.</p>
<p>I did not make this workflow, it was Mickmumpitz. Watch his entire video to learn how this workflow works, I will not repeat what he already explains:</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/grtmiWbmvv0"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>I will also not share his workflow, because he has a paid Patreon (it is super cheap, only USD 10 per month), which gives you access to download his entire workflow pack <a href="https://www.patreon.com/posts/free-workflows-120405048"target="_blank" rel="noopener">at this link</a></p>
<p>But that alone is not enough. The requirements you need:</p>
<ul>
<li>a beefy machine with at least an RTX 3090 with 24GB of VRAM. The important thing here is VRAM, the models used easily exceed 20GB of usage. CPU is less important, I use a 7950X3D but a 7800 would already handle it.</li>
</ul>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/730safwxqkrsgawu4liw42scoul3" alt="GPU VRAM"  loading="lazy" /></p>
<ul>
<li>if you do not have such a machine, it takes more work but you can rent one on <strong>RUNPOD</strong>. They have several GPU configurations, but you will need to know how to configure Linux a bit or choose Windows and do Mickmumpitz&rsquo;s tutorial all by hand (trust me, you will suffer if you do it that way).</li>
<li>you need to have at least <strong>500GB</strong> of available space and good internet to download. The first time my Docker runs, it automatically downloads all ComfyUI extensions and all models for everything. You can edit the <code>models.conf</code> file beforehand and remove models that you know you will never use. But just this workflow alone requires at least about 30GB of models, there is no way around it.</li>
<li>this workflow is particularly <strong>HEAVY</strong> because it has several features (parts of which we can disable, to customize for our workflow). But for 1 character, the entire flow can take <strong>30 MINUTES</strong>. Even on an RTX 4090 like mine.</li>
</ul>
<p>If you have done everything right up to this point, you have ComfyUI running with all dependencies already downloaded and you bought the Workflow. When you load it you will have to fix some things manually.</p>
<p>The first thing is that you will see an error in several Nodes in red, all of them <strong>Ultimate SD Upscale</strong>. Just right-click on each one and choose &ldquo;Fix Recreate&rdquo; and it will fix automatically:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/djblur5g36mw9denng0ht0vvksym" alt="Fix Upscale"  loading="lazy" /></p>
<p>ComfyUI requires that each output slot of a Node connects to the input slot of another compatible Node, but often the workflows get so big that there are little wires connecting things scattered all over the place and it gets really hard to know what connects to what. It literally becomes <strong>spaghetti</strong>.</p>
<p>But there is an extension (which I have already pre-installed) called <a href="https://github.com/chrisgoringe/cg-use-everywhere"target="_blank" rel="noopener">cg-use-everywhere</a>. Every extension is a little GitHub project, cloned into the <code>custom_nodes</code> subdirectory. It lets you connect a Node&rsquo;s output to this <strong>Anything Elsewhere</strong> node, say the &ldquo;clip&rdquo; output of the &ldquo;Load Clip&rdquo; Node. Now, every other Node that has &ldquo;clip&rdquo; as an input field and does not have anything explicitly connected to it will use this Anything that was created. It works super well.</p>
<p>Only in this Mick workflow, who knows why, all the Anywheres are broken. But it is easy to fix. Just find these Nodes, delete them, create new ones and reconnect. There are several right at the beginning, write down who connects to each one, delete them and create a new one to reconnect:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/kjjo7u6g58drww1v5f8v88h1iis1" alt="anywhere initial"  loading="lazy" /></p>
<p>There are also some in the Upscale group 02:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/qrkdxs6qjdtmdjs3c463oxetbgyy" alt="anywhere upscale"  loading="lazy" /></p>
<p>Small Nodes, with only the title showing, are COLLAPSED. Just right-click and choose EXPAND to see the details. It is another way to organize the mess.</p>
<p>Lastly, you have to recheck the correct name of the models in all the &ldquo;Load&rdquo; something Nodes right at the beginning of the workflow (luckily he organized these nodes all together):</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/y1u93ujz3kq4hz3pik5q4wcgapoa" alt="model path"  loading="lazy" /></p>
<p>In his workflow, the Node comes looking for &ldquo;FLUX/flux1-dev-fp8.safetensors&rdquo; but then you go to the list (which pulls from what is actually in the &ldquo;models/checkpoints&rdquo; directory on your machine) and choose the one with the same name. This happens often because different people organize different subdirectories for the models. But after configuring, just save and you are done. You have to do this for the checkpoint, controlnet, ic-light, etc.</p>
<p>ComfyUI errors are not friendly. They are Python stacktraces, which are not friendly because they show which line of code blew up, but they do not show what was in the variables passed to the function that blew up. So there is no way to know where it went wrong. But normally on the web interface, the Node turns red, then you check it and usually it is because you forgot to download the model or pick the right one from the list.</p>
<p>The last thing in this workflow is to download the OpenPose image from his Patreon page, which I linked at the beginning of the post, and load it:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/uajcc6ocnr5llzv8wzoowl85c5o3" alt="openpose image"  loading="lazy" /></p>
<p><strong>OpenPose</strong> is a standard file format that describes poses, an open format that can be edited in several tools. And there are several pre-trained models to extract poses in this format from images, so we do not even need to edit it by hand. We can download several pre-made open poses from this site:</p>
<p><a href="https://openposes.com/"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/ntvaf9vba1rugg11qn3n14u2t69e" alt="Poses Sites"  loading="lazy" /></a>
I am going to repeat this several times, but <strong>watch Mick&rsquo;s video tutorial</strong> where he explains how to generate characters from scratch or use <strong>your own photos</strong> with your own poses to continue the flow from there. His workflow, after running, looks like this (with zoom way down below):</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/jqu4o2c5cenci6au2g8bbny064mm" alt="complete workflow"  loading="lazy" /></p>
<p>In the first part, there is the manual configuration and checking of the models. You only have to fix it once and save. After that it will always work the same way. In this first menu you can also turn on and off some of the Node groups, for example, to skip the first stage of generating a character from scratch and be able to use your own photos:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/s72mrc1ib5o173c9l74vmpsn3js3" alt="menu and setup"  loading="lazy" /></p>
<p>If you choose to generate a character from scratch, there is a green prompt box to type how you want your character to be, in what style. Text Prompts are decoded by <strong>CLIP</strong> (Contrastive Language Image Pre-Training) models, which are specific to certain UNET categories (like SDXL for Stable Diffusion or the now best and most popular FLUX). Part of the learning is starting to figure out which sub-models (clips, clip_vision, vaes, loras, etc.) are compatible with which unets (unet, checkpoints, diffusion_models). You cannot mix just anything. Roughly speaking, it is a bit like, if you choose to do a text in German, all plugins have to be in German.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/37oe4dyqr5t0in9ad98bocli6u3w" alt="Character Generation"  loading="lazy" /></p>
<p>I will not show the complete workflow because it does not even fit, but you can see the prompts in green, in the middle where we choose which pose we want (it is on his Patreon), and on top the image generated by the <strong>flux1-dev-fp8</strong> model, one of the best and most popular for having excellent quality and not being so heavy (fp8 instead of fp32, for example).</p>
<p>This is one of the advantages of using tools outside the Dalle or Midjourney of the world: we can control all aspects of image generation not only with text prompts, but also with image prompts, like OpenPose.</p>
<p>Another detail. I explained in my previous post about KSampler or Samplers in general, that they are the engine that does all the work of joining all the models, images, prompts, controlnet, vaes and produces the final image. KSampler parameters like steps, cfg, denoise, scheduler and more <strong>DEPEND</strong> on the chosen model. There are well-documented models, there are poorly documented models.</p>
<p>For example, say you read on a Reddit thread about this other more optimized model called <a href="https://civitai.com/models/215418?modelVersionId=273102"target="_blank" rel="noopener">&ldquo;TurboVisionXL&rdquo;</a>, which is available on the Civit.ai site:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/hpcj5e8g0hxvefelvszwf48vsnpf" alt="TurboVisionXL"  loading="lazy" /></p>
<p>It really is a good model, but now what? What do I configure in the KSampler for it? Luckily if you scroll the page (always read the original documentation!!) you will find these recommendations:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/57f9vyfh4w9j8bduaomc3aow2sks" alt="TurboVisionXL documentation"  loading="lazy" /></p>
<p>See? He explains what to configure and what the known limitations are. Further down there will be comments from other people who can give more tips or share errors that you have already seen. It is a huge community, you need to learn how to participate in it.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/7avk9tl1pop1eexkuu3h8epgs2t5" alt="MultiView"  loading="lazy" /></p>
<p>But back from the tangent, in Mick&rsquo;s second group of nodes, he takes the first image in &ldquo;Pose T&rdquo; and runs it through a &ldquo;Multi View&rdquo; process using Diffuser MV which does this automatically. It is the same pose, but as if the camera went 360 degrees around it, capturing different angles and, most importantly, generating new images coherent with the original (see how the model is even used to invent the character&rsquo;s back).</p>
<p>In this step he also extracts only the face from the first image to do other treatments in parallel. Face recognition and extraction is super common. Every security system uses it, Google Photos uses it to organize dynamic albums. And it is worth learning to use in your own products. It is a reasonably light and fast process these days and there are several pre-made algorithms and models to use. In this case I do not think it even uses A.I. as such, it is just an algorithm.</p>
<p>Another detail: image generation models normally work in <strong>LOW RESOLUTION</strong>. Usually below 1024x1024. Do not expect to generate 4K images right off the bat. But that is no problem, because the solution is to first generate a low resolution image and then run it through another A.I. model specialized in <strong>UPSCALING</strong> like the RealESRGAN that I already talked about in my other post about <a href="https://www.akitaonrails.com/en/2025/04/19/aumentando-resolucao-de-anime-velho-pra-4k-com-i-a"target="_blank" rel="noopener">upscaling old anime</a></p>
<p>Different from what Photoshop or any image editor does, if you try to enlarge an image, it just takes a pixel and tries to duplicate, quadruplicate the surrounding pixels. But that usually only makes the image bigger but blurred, because it has no way of knowing the details, and it will not even try to &ldquo;invent&rdquo; details.</p>
<p>To &ldquo;invent&rdquo; details, you need a pre-trained model that learned what to draw. It is also how NVIDIA&rsquo;s DLSS 3 works for games, where the game renders in low resolution, like 720p, and DLSS, in real time, redraws the new screen in 4K. It is not perfect, but playing at 60fps, you will rarely notice the difference. You will only notice that it got more <strong>CRISP</strong>.</p>
<p>That is also why, if you use your own photo, or photos of people you know, you will see that the new photo after upscaling looks <strong>slightly different</strong>, it is no longer the same person, because the model <strong>REDRAWS</strong> details that did not exist. It is a process of interpretation and not of discovering &ldquo;hidden&rdquo; details (that does not exist, only in CSI fiction).</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2he2fgmus8vmp3wfp95s3qbh7ztf" alt="Upscale 2"  loading="lazy" /></p>
<p>Mick&rsquo;s workflow goes one step further and has a second upscaling pass, focusing on fixing only the face with a Face Detailer Node, made specifically to fix and improve faces. And here we enter plastic surgery and harmonization territory for real lol the final face will be different from the original. But for NPCs and secondary characters it does not matter, nobody will know the difference anyway, as long as it is not deformed, cross-eyed or something else that jumps out at you.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/3d40a8n9b32mmkub5l8c04oou37h" alt="Emotions"  loading="lazy" /></p>
<p>There are Nodes specialized in expressions, like the Expression Editor (PHM) which starts from a single face photo and manages to generate new photos in the most varied emotions - which we can configure exactly, numerically, however we want. I will not show it, but note that the generated images are in low quality, but the next step in the workflow is precisely to upscale them too.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/cxdua4dfalu6d68tpaleljie5y1e" alt="Lighting"  loading="lazy" /></p>
<p>This step is interesting. First the top part, with the various scenarios. They were all generated via the green prompts below them, prompts that Mick left but that we can edit however we want OR swap the generation node and put a Load Image node to load our own background images, from real photos.</p>
<p>The objective of this step is to generate the faces under different light configurations. Many people think that &ldquo;light&rdquo; in games is just &ldquo;turn on the red light&rdquo; and everyone will automatically turn red. In new games, with <strong>RAY TRACING</strong> yes, that is why it is SUPER HEAVY. But in games made for old machines, indie games, you cannot demand this from everyone.</p>
<p>So &ldquo;tricks&rdquo; are used to fake things like lights and shadows. It is the <strong>BAKING</strong> process where we literally &ldquo;stamp the light onto the texture&rdquo; of objects and characters. With global illumination on or off, they will always be &ldquo;painted&rdquo; with the colored light of the scene. To switch scenes, new textures are loaded with other &ldquo;baked&rdquo; lights. For programmers, think of baking as <strong>Caching</strong>. Why recalculate everything dynamically every time, if the scene&rsquo;s light never changes? Save that step and leave it &ldquo;cached&rdquo;.</p>
<p>Going back, this step uses the <a href="https://github.com/lllyasviel/IC-Light"target="_blank" rel="noopener"><strong>IC-Light</strong></a> technology which is another open source project worth learning, even if you are not interested in A.I. It can manipulate the light of any image or photo. You, as a photographer, can change the light if you have changed your mind, in post-production. Adobe or BlackMagic tools, I think they already include similar technology, but it is good to know that there is open source and you can integrate it into your product.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/qup2n6vadpc7qrtgokbqbfkpvmjg" alt="final sheet"  loading="lazy" /></p>
<p>After generating the faces with different lights, obviously there is another upscaling step - which I am skipping showing - and it goes to the final group of nodes that joins everything into a &ldquo;sheet&rdquo;, a &ldquo;character sheet&rdquo;. The game developer can use it directly in a 2D game (think of a visual novel game), or it can serve as a basis for UV Mapping textures of a simple 3D model with Armature to animate and you already have some emotional faces. Good for placing in the background scenery, pedestrians walking on the street, shopping, sitting in the square or something like that.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/cgv0qp3slipgw4nrlgnrserl1n8s" alt="Loras"  loading="lazy" /></p>
<p>In addition, Mick&rsquo;s tutorial describes how it is possible to create <strong>LORAS</strong> (Low-Rank Adaptation). If you want to be able to generate characters with a specific style or even specific faces (without &ldquo;artistic&rdquo; intervention from the A.I.), you choose a model like FLUX or SDXL and &ldquo;on top&rdquo; of it you add a second compatible sub-model with information about that person or character.</p>
<p>For this you take several clean and well-lit photos of the person, from many different angles. You do not even need much, just a dozen is enough. For each image you create prompts that describe it well, and that is it. There are tools to generate these Loras locally or sites that do this (watch the video).</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/tk8tbtrr1ealm4c4eu26alk63xlh" alt="Creating Prompts"  loading="lazy" /></p>
<p>Speaking of prompts, in several workflows there are steps to describe detailed prompts of dynamically generated images. Instead of requiring the user (you) to do this manually, you can use a decoding model, which decodes images into text prompts. The most used model for this is Microsoft&rsquo;s <strong>FLORENCE 2</strong>.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/dwr87ajtkj7w15ftsy86l5u4hz44" alt="Florence 2"  loading="lazy" /></p>
<p>That text box at the bottom is read-only, you do not need to type it manually. The Florence 2 Run Node will read the image and generate this text. See how it described the first image of the woman in the &ldquo;T&rdquo; pose. Doing tests, for your case, you determine if just this is enough; if it is not, just put a second prompt Node and concatenate the two before sending them to other nodes.</p>
<p>Anyway, there is A LOT to explore just in this workflow. Mick did an excellent job both on the workflow and on the video to explain it, and it is worth paying the 10 dollars to contribute. There are dozens of workflows like this available on the Web, just search. And my setup is getting more and more complex. I plan to keep adjusting it for each new interesting workflow I run into. There are already several pre-tested ones in the &ldquo;workflows&rdquo; folder of my project for you to play with.</p>
]]></content:encoded><category>generative-media</category><category>gaming</category><category>local-models</category></item></channel></rss>