If you have ever considered swapping out the API calls your coding harness is making for a local model it probably ended in disappointment. You ran a quick llama-bench and thought you were going to see X tokens/second. What that benchmark doesn’t tell you is how it actually feels to use these harnesses. The development experience is extremely variable. You might have sat there for minutes before seeing any response. Maybe you were getting somewhere and halfway through it started stalling. ‘Who is this prefill, taking all my time?’ It’s not your fault: most coding harnesses weren’t built with a local model in mind.
This is not a hit job on the excellent work of other teams. I’m trying to reliably measure what happens when you swap out the data center for localhost. A certain level of wrong tool for the problem is to be expected. Please add salt: I have been tinkering with chad, a coding harness optimized specifically for Qwen 3.8 27B on Apple silicon.
Most harnesses conspire against localhost in three ways.
Large system prompts & tool schemas. Let’s assume your laptop reads at 90 tokens per second and writes about 10. These represent the prefill & generation parts of every loop in your coding harness. At these speeds every 1,000 tokens of reading a prompt translates to ~ 11 seconds of staring at the cursor before the model begins to write. Before your LLM does any work it will read the entire system prompt along with any loaded tool schemas. In the pi harness this combination was 2,008 tokens for Qwen 3.8 27B, but 18,046 all in for Opencode. You won’t notice much difference when you have a data center GPU with prefill rates averaging 10k+ tokens/second. This collapses to 0.2 versus 1.8 seconds. On your laptop? That is the difference between 22 and 226 seconds, measured. Unbearable!
Smaller context windows. After your LLM finishes the system prompt you have a finite context window left in memory to do work. It is smaller than you think, and your harness just spent part of it. How much context you have depends on how much memory you are starting with vs. how big the model’s weights are. There are a lot of variables here, but 32,000 tokens is a reasonable guess at how much room you’ll have left with a reasonably good model on a reasonably good laptop. How much of that 32,000 token budget is left for the pi harness? 94% - seems manageable. Opencode? With 18,046 tokens out the door already only 44% of your context is left for actually doing work.
A habit of side requests. In the traditional local client / remote server pattern the harness can make as many side requests to the data center servers as it likes. Your laptop is both the client and the server. In the best case those side requests cause the local model to queue and wait. In the worst case they cause repeated long prefills. Over 24 tasks opencode fired 33 of them, crush 51 and dsh 24 (session titles and summaries), almost every one overlapping an agent turn. The model was “busy” 125% and 114% of wall clock for opencode and crush: two requests in flight on one GPU.
I put 9 harnesses through a series of 8 Exercism exercises, each in its own auto-approve mode, with one identical one-sentence prompt. Each task leveraged the same M4 MacBook Pro (24GB, macOS 26.6.2) with a 3 bit quant of Qwen 3.8 27B model served via llama.cpp (build 10470). The same llama-server was shared by every harness and a common proxy enforced the same recommended sampling regime for Qwen (temperature=1.0, top_k=20, top_p=0.95, min_p=0.05). Every session had the same 32,768 unified cache served across four slots. Every number reported below is llama-server's own accounting as read through the proxy, never a harness self-report, except the two rows marked * (chad on its in-process MLX engine, where there is no server to observe, so they come from chad's own prefill trace with the same definitions).
| harness | version | tools | tax: turn-1 prompt (tok) | wait before 1st token, turn 1 | wait / later turn (med · p90) | cache reuse | exp. tok/s | pass (gate) |
|---|---|---|---|---|---|---|---|---|
| mini-swe-agent | 2.4.6 | 1 | 1,171 | 12.2 s | 3.6 s · 21 s | 96% | 8.0 | 11/24 (14 T) |
| pi | 0.80.3 | 4 | 2,008 | 21.6 s | 1.3 s · 22 s | 99% | 8.1 | 19/24 (7 T) |
| cline | 3.0.60–61 | 26 | 5,876 | 64.1 s | 9.9 s · 52 s | 94% | 7.3 | 17/24 |
| codex | 0.151.0 | 10 | 7,804 | 87.8 s | 9.6 s · 28 s | 94% | 6.9 | 19/24 (5 T) |
| dsh | 0.1.1-rc.2 | 25 | 8,052 | 94.4 s | 2.2 s · 34 s | 99% | 7.2 | 18/24 |
| goose | 1.50.0 | 18 | 9,617 | 110.3 s | 1.0 s · 22 s | 100% | 8.0 | 22/24 (3 T) |
| crush | 0.92.0 | 26 | 16,263 | 199.8 s | 1.8 s · 40 s | 100% | 5.8 | 18/24 (8 T) |
| opencode | 1.17.12 | 10 | 18,046 | 225.7 s | 4.6 s · 44 s | 99% | 5.7 | 15/24 (13 T) |
| chad (llama.cpp) | 2.0.3 | 5 | 2,563 | 25.6 s | 0.8 s · 19 s | 99% | 7.9 | 24/24 |
| chad (MLX, serial) * | 2.0.3 | 5 | 2,566 | 4.7 s | 1.0 s · 19 s | 99% | 12.4 | 21/24 (3 T) |
| chad (MLX, dflash2) * | 2.0.3 | 5 | 2,562 | 4.6 s | 0.9 s · 36 s | 99% | 17.4 | 22/24 (3 T) |
In my experience the harnesses fell into three groups.
Heavy but disciplined (dsh, cline, codex, goose). The combined effect of either long system prompts or a high tool schema count. Their prefix is byte stable so once you get started it feels tolerable. (goose earned this group at 1.50.0: earlier releases re-rendered a minute-resolution timestamp into the first user message every turn. This tanked cache reuse to 78%)
Heavy to start (crush, opencode). You’ll wait 3-4 minutes before you see any activity.
None of these harnesses are badly engineered. Opencode’s 18k system prompt is there because it helps frontier models deployed behind an API. The 26 tool schemas in crush are fine when you have 200k context and prefill is instantaneous. All of these choices were made in an environment where the prefill is virtually free. At local speeds this all falls apart.