<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Remdore]]></title><description><![CDATA[Remdore]]></description><link>https://remdore.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>Remdore</title><link>https://remdore.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Tue, 22 Sep 2026 11:07:25 GMT</lastBuildDate><atom:link href="https://remdore.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[I asked 27 models what 2+2 is. The answers cost between 2 and 159 tokens, and one was wrong half the time.]]></title><description><![CDATA[Same question, same endpoint, same key. Eight of the 27 models answered "4" in two completion tokens. qwen3.5-397b-a17b spent 159 tokens and 24 seconds on it, and was no more correct. The difference i]]></description><link>https://remdore.hashnode.dev/i-asked-27-models-what-2-2-is-the-answers-cost-between-2-and-159-tokens-and-one-was-wrong-half-the-time</link><guid isPermaLink="true">https://remdore.hashnode.dev/i-asked-27-models-what-2-2-is-the-answers-cost-between-2-and-159-tokens-and-one-was-wrong-half-the-time</guid><category><![CDATA[AI]]></category><category><![CDATA[llm]]></category><category><![CDATA[Python]]></category><category><![CDATA[APIs]]></category><category><![CDATA[Machine Learning]]></category><dc:creator><![CDATA[Kalin Dimitrov]]></dc:creator><pubDate>Mon, 21 Sep 2026 17:02:42 GMT</pubDate><content:encoded><![CDATA[<p>Same question, same endpoint, same key. Eight of the 27 models answered "4" in two completion tokens. <code>qwen3.5-397b-a17b</code> spent 159 tokens and 24 seconds on it, and was no more correct. The difference is hidden reasoning you are billed for, the switch that turns it off differs on nearly every model, and three models have no switch at all.</p>
<p>Ask for a two-word coffee shop name instead and the gap gets worse: four tokens at the low end, 977 on <code>kimi-k2.6</code>. That is a 244x difference in what you are billed for the same two words of output.</p>
<p>The tokens are going into a hidden reasoning trace the model writes before it writes your answer. You pay for that trace, it usually does not reach you, and on most of these models <code>usage</code> does not even tell you it happened. This is a measurement of how much it costs, which parameter switches it off, and why that parameter is different on nearly every model.</p>
<h2>What was measured</h2>
<p><code>https://inference.do-ai.run/v1</code>, 21 September 2026, all 27 chat models my subscription tier can call. Three prompts with short correct answers:</p>
<ul>
<li><code>What is 2+2? Reply with only the number.</code></li>
<li><code>What is the capital of France? Reply with only the city name.</code></li>
<li><code>Invent a two-word name for a coffee shop. Reply with only the name.</code></li>
</ul>
<p>Three runs each, recording <code>usage.completion_tokens</code>, the visible <code>message.content</code>, the length of <code>message.reasoning_content</code>, and wall-clock latency. The interesting number is billed tokens against the fewest tokens any model needed for that same answer. Eight models said "4" in two tokens, so two is the floor, and everything above it is overhead.</p>
<h2>The spread</h2>
<p>Completion tokens, median of three runs, for "What is 2+2?":</p>
<table>
<thead>
<tr>
<th>Model</th>
<th>Tokens</th>
<th>vs floor</th>
</tr>
</thead>
<tbody><tr>
<td>deepseek-v4-pro, mistral-3-14B, gemma-4-31B-it, llama-4-maverick, qwen3.8-max, deepseek-3.2, deepseek-4-flash, deepseek-v4-pro-0813</td>
<td>2</td>
<td>1x</td>
</tr>
<tr>
<td>glm-5.3, glm-5.3-flash</td>
<td>3</td>
<td>1.5x</td>
</tr>
<tr>
<td>router:software-engineering</td>
<td>9</td>
<td>4.5x</td>
</tr>
<tr>
<td>deepseek-v4-flash-0731</td>
<td>16</td>
<td>8x</td>
</tr>
<tr>
<td>deepseek-v4.1-flash</td>
<td>21</td>
<td>10.5x</td>
</tr>
<tr>
<td>openai-gpt-oss-20b</td>
<td>35</td>
<td>17.5x</td>
</tr>
<tr>
<td>openai-gpt-oss-120b</td>
<td>38</td>
<td>19x</td>
</tr>
<tr>
<td>nemotron-3-ultra-550b</td>
<td>39</td>
<td>19.5x</td>
</tr>
<tr>
<td>kimi-k3</td>
<td>67</td>
<td>33.5x</td>
</tr>
<tr>
<td>kimi-k2.6</td>
<td>89</td>
<td>44.5x</td>
</tr>
<tr>
<td>glm-5.2</td>
<td>103</td>
<td>51.5x</td>
</tr>
<tr>
<td>minimax-m2.5</td>
<td>108</td>
<td>54x</td>
</tr>
<tr>
<td>qwen3.5-397b-a17b</td>
<td>159</td>
<td>79.5x</td>
</tr>
</tbody></table>
<p>Eleven of the 27 sit at or near the floor. Sixteen emit a reasoning trace nobody asked for.</p>
<h2>The expensive answers were not better, but the cheap ones were not always right</h2>
<p>The obvious defence is that the expensive models are being careful. On these prompts they are not buying anything with it. Every model in the top half of that table returned "4" and "Paris" on every run, and the 159-token answer is the same single character as the 2-token one.</p>
<p>What surprised me is which end produced the errors. Both of them came from the cheap end. <code>deepseek-4-flash</code> returned "5" once, and <code>deepseek-v4-pro-0813</code> answers "2" about half the time, which gets its own section further down because the reason is worth reading. Not one of the heavy reasoners got the arithmetic wrong.</p>
<p>So this is not an argument that reasoning is waste. Reasoning earns its keep on hard problems. The point is narrower: these models cannot tell that "2+2" is not a hard problem, so they spend on it the same way they would spend on something that deserves it.</p>
<h2>The off switch is different on almost every model</h2>
<p>Sixteen models reason unprompted. Thirteen of them can be stopped, and this is where it gets tedious: there is no single parameter that does it.</p>
<p><code>reasoning_effort: "none"</code> works on 11 models. <code>chat_template_kwargs: {"enable_thinking": false}</code> works on 12. The overlap is partial, so you cannot pick one and ship it.</p>
<p>Verified separately at ten runs each, with the answer checked every time:</p>
<table>
<thead>
<tr>
<th>Model</th>
<th>Before</th>
<th>After</th>
<th>Switch</th>
</tr>
</thead>
<tbody><tr>
<td>qwen3.5-397b-a17b</td>
<td>166</td>
<td>2</td>
<td><code>reasoning_effort: "none"</code></td>
</tr>
<tr>
<td>glm-5.2</td>
<td>98</td>
<td>2</td>
<td><code>chat_template_kwargs: {"enable_thinking": false}</code></td>
</tr>
<tr>
<td>kimi-k2.6</td>
<td>63.5</td>
<td>2</td>
<td><code>reasoning_effort: "none"</code></td>
</tr>
<tr>
<td>openai-gpt-oss-120b</td>
<td>38</td>
<td>16</td>
<td><code>reasoning_effort: "low"</code></td>
</tr>
</tbody></table>
<p>All four answered "4" in 10 of 10 runs with the switch on. An 83x reduction on <code>qwen3.5-397b-a17b</code> for one parameter, and the answer does not change.</p>
<p><code>glm-5.2</code> is the awkward one. It rejects <code>reasoning_effort</code> outright:</p>
<pre><code>400 reasoning_effort must be one of [medium high xhigh] for this model
</code></pre>
<p>There is no "low" and no "none" in that list, so the documented OpenAI parameter cannot ask it for less thinking at all. The only thing that works is the vLLM template flag.</p>
<h2>Three models you cannot turn off</h2>
<p><code>minimax-m2.5</code>, <code>openai-gpt-oss-120b</code> and <code>openai-gpt-oss-20b</code> never stop reasoning whatever you send.</p>
<p>The two <code>gpt-oss</code> models at least let you turn it down. <code>reasoning_effort: "low"</code> takes them from 38 to 16 tokens. <code>none</code> and <code>minimal</code> are refused with an error that names the chat format rather than the model:</p>
<pre><code>400 Harmony does not support reasoning_effort='none'
</code></pre>
<p><code>minimax-m2.5</code> accepts every switch with a 200 and ignores all of them, which is the worst of both worlds because nothing tells you it did nothing.</p>
<p>I nearly got this one wrong. My first pass ran three samples per variant and reported that <code>thinking: {"type": "disabled"}</code> cut <code>minimax-m2.5</code> from 105 tokens to 60. Ten runs later: baseline median 74, with the parameter 76, and a baseline range of 55 to 175 tokens. The variance on that model is wider than the effect I thought I had found. Three samples was not a measurement.</p>
<h2><code>reasoning_effort: "low"</code> can make things worse, not better</h2>
<p>This one caught me while I was testing the script at the bottom of this post.</p>
<p>If a model does not reason by default, asking it for a low reasoning effort does not reduce anything. It switches reasoning on. "Low" is not a step down from the default, it is a step up from nothing. Ten runs each:</p>
<table>
<thead>
<tr>
<th>Model</th>
<th>Default</th>
<th>With <code>reasoning_effort: "low"</code></th>
</tr>
</thead>
<tbody><tr>
<td>deepseek-3.2</td>
<td>2</td>
<td>86</td>
</tr>
<tr>
<td>gemma-4-31B-it</td>
<td>2</td>
<td>52.5</td>
</tr>
<tr>
<td>qwen3.8-max</td>
<td>2</td>
<td>42</td>
</tr>
<tr>
<td>deepseek-v4-pro</td>
<td>2</td>
<td>32.5</td>
</tr>
<tr>
<td>llama-4-maverick</td>
<td>2</td>
<td>2</td>
</tr>
<tr>
<td>mistral-3-14B</td>
<td>2</td>
<td><code>400</code>, rejected</td>
</tr>
</tbody></table>
<p><code>deepseek-3.2</code> goes from 2 tokens to 86, a 43x increase, for setting the parameter people reach for when they want to spend less. Every one of those runs still answered "4".</p>
<p>So the parameter is not a dial from expensive to cheap. On a reasoning model it turns the volume down, and on a non-reasoning model it turns the feature on. You have to know which kind you are talking to before the parameter means anything.</p>
<h2>The latency is worse than the token count</h2>
<p>Tokens are money, but the wait is what your users feel. Median wall-clock for "What is 2+2?", six runs:</p>
<table>
<thead>
<tr>
<th>Model</th>
<th>Before</th>
<th>After</th>
<th>Switch</th>
</tr>
</thead>
<tbody><tr>
<td>qwen3.5-397b-a17b</td>
<td>24.35s</td>
<td>0.97s</td>
<td><code>reasoning_effort: "none"</code></td>
</tr>
<tr>
<td>kimi-k2.6</td>
<td>1.74s</td>
<td>0.64s</td>
<td><code>reasoning_effort: "none"</code></td>
</tr>
<tr>
<td>glm-5.2</td>
<td>1.84s</td>
<td>0.79s</td>
<td><code>chat_template_kwargs</code></td>
</tr>
</tbody></table>
<p>Twenty-four seconds to answer "4". One parameter takes it under a second.</p>
<h2><code>usage</code> does not tell you what you paid for</h2>
<p>OpenAI reports hidden reasoning in <code>usage.completion_tokens_details.reasoning_tokens</code>. Here, 8 of 27 models populate that field, and the ones that populate it are mostly the <code>router:</code> targets rather than the models actually doing the thinking.</p>
<p>So on a model like <code>glm-5.2</code> you get a <code>completion_tokens</code> of 103 for a one-character answer and no breakdown explaining it. The only way to see the trace is <code>message.reasoning_content</code>, which is not in the OpenAI schema, so a typed client will drop it before you ever look.</p>
<h2>One model answers "2" to "2+2", and a space fixes it</h2>
<p>This one is worth the whole exercise.</p>
<p><code>deepseek-v4-pro-0813</code> answers the question wrong. Thirty runs of <code>What is 2+2? Reply with only the number.</code> and it replies "2" fifteen times. Fifty per cent, on arithmetic a calculator settled in 1642.</p>
<p>It is not that the model cannot add. Thirty runs of each variation:</p>
<table>
<thead>
<tr>
<th>Prompt</th>
<th>Wrong</th>
</tr>
</thead>
<tbody><tr>
<td><code>What is 2+2? Reply with only the number.</code></td>
<td>15/30</td>
</tr>
<tr>
<td><code>What is 2+2?</code> (no constraint)</td>
<td>0/30</td>
</tr>
<tr>
<td><code>What is 2 + 2? Reply with only the number.</code></td>
<td>0/30</td>
</tr>
<tr>
<td><code>What is two plus two? Reply with only the number.</code></td>
<td>0/30</td>
</tr>
<tr>
<td><code>What is 17+25? Reply with only the number.</code></td>
<td>0/30</td>
</tr>
</tbody></table>
<p>Drop the output constraint and it is perfect, showing its working and arriving at 4. Spell the numbers out and it is perfect. Ask it something harder, <code>17+25</code>, and it answers 42 every time.</p>
<p>Add one space, <code>2 + 2</code>, and the failure disappears completely.</p>
<p>The trigger needs both halves: the unspaced digit string and the instruction to reply with only the number. My read is that <code>2+2</code> tokenizes as a unit the model handles badly under a hard output constraint, and asked for a bare number it emits the first one it saw. I have not confirmed that, and I am labelling it a guess.</p>
<p>The sibling model matters here. <code>deepseek-v4-pro</code>, same family, no date suffix, is 0 wrong in 30 on the identical prompt. Two snapshots of the same model, one of them 50% wrong on a prompt the other never misses. If you pin a dated model id for reproducibility, this is what you are pinning.</p>
<h2>One model leaks its chat template into <code>content</code></h2>
<p><code>nemotron-nano-12b-v2-vl</code> puts raw ChatML markup in the visible answer. Six of nine runs across the three prompts:</p>
<pre><code>'&lt;|im_start|&gt;answer&lt;|im_end|&gt;4\n&lt;|im_start|&gt;'
'&lt;tool_call&gt;\n{"name": "get_capital", "arguments": {"country": "France"}}\n&lt;/tool_call&gt;\n&lt;|im_end|&gt;\n'
</code></pre>
<p>The request carried no <code>tools</code> key at all. The model invented a function, the API did not parse it into <code>tool_calls</code> (that field is <code>None</code>), so the markup arrives as ordinary text in <code>message.content</code>. Anything rendering that straight to a user displays a fake tool call and a stop token.</p>
<p>It was the only model of the 27 that did this.</p>
<h2>What to do with this</h2>
<p>Measure your own. The numbers above are one endpoint on one afternoon and they will drift.</p>
<p>If you are calling a reasoning model for something trivial, find its off switch before you ship, and expect it to be model-specific rather than provider-specific. Try <code>reasoning_effort: "none"</code>, then <code>chat_template_kwargs: {"enable_thinking": false}</code>, and check <code>usage.completion_tokens</code> rather than trusting the parameter was honoured, because <code>minimax-m2.5</code> will accept all of them and change nothing.</p>
<p>Use enough repeats to see past the variance. Three was not enough for me and it cost me a wrong result I nearly published.</p>
<p>And if you are pinning a dated model snapshot, run your actual prompts against it. <code>deepseek-v4-pro-0813</code> is two tokens and fast and wrong half the time, and nothing in the response would tell you.</p>
<h2>The script</h2>
<pre><code class="language-python">#!/usr/bin/env python3
"""What does a one-token answer cost on your endpoint, and can you turn it off?"""
import json, os, statistics, urllib.request

BASE = os.environ.get("BASE_URL", "https://inference.do-ai.run/v1")
KEY = os.environ["API_KEY"]
Q = "What is 2+2? Reply with only the number."
REPS = 10

SWITCHES = {
    "baseline": {},
    "reasoning_effort=none": {"reasoning_effort": "none"},
    "reasoning_effort=low": {"reasoning_effort": "low"},
    "enable_thinking=false": {"chat_template_kwargs": {"enable_thinking": False}},
}

def call(payload):
    req = urllib.request.Request(
        BASE + "/chat/completions", data=json.dumps(payload).encode(), method="POST",
        headers={"Authorization": "Bearer " + KEY, "Content-Type": "application/json"})
    try:
        with urllib.request.urlopen(req, timeout=120) as r:
            return json.loads(r.read().decode())
    except Exception as e:
        return {"_error": str(e)[:80]}

def measure(model, extra):
    toks, ok = [], 0
    for _ in range(REPS):
        d = call({"model": model, "max_tokens": 4000, **extra,
                  "messages": [{"role": "user", "content": Q}]})
        if "_error" in d:
            return None
        toks.append(d["usage"]["completion_tokens"])
        content = d["choices"][0]["message"].get("content") or ""
        # an empty or wrong answer makes a low token count meaningless
        if "4" in content:
            ok += 1
    return {"median": statistics.median(toks), "min": min(toks),
            "max": max(toks), "correct": ok}

for model in os.environ.get("MODELS", "deepseek-v4-pro").split(","):
    print("\n" + model)
    for name, extra in SWITCHES.items():
        m = measure(model, extra)
        if m is None:
            print("  %-24s rejected" % name)
        else:
            print("  %-24s median=%-5s range=%s-%s  correct=%d/%d"
                  % (name, m["median"], m["min"], m["max"], m["correct"], REPS))
</code></pre>
<p>Run it with <code>MODELS=glm-5.2,kimi-k2.6,qwen3.5-397b-a17b</code>. Compare the medians, not single calls, and check the <code>correct</code> column before you celebrate a low token count. Two tokens and a wrong answer is not a saving.</p>
]]></content:encoded></item><item><title><![CDATA[Show a model your old code and it writes your old bugs: 32 runs, 0% reuse]]></title><description><![CDATA[Last July I spent seven pull requests deleting the same component eleven times. Eleven games in my football quiz app had each grown their own search box, and they had drifted apart in the way duplicat]]></description><link>https://remdore.hashnode.dev/show-a-model-your-old-code-and-it-writes-your-old-bugs-32-runs-0-reuse</link><guid isPermaLink="true">https://remdore.hashnode.dev/show-a-model-your-old-code-and-it-writes-your-old-bugs-32-runs-0-reuse</guid><category><![CDATA[AI]]></category><category><![CDATA[Programming Blogs]]></category><category><![CDATA[refactoring]]></category><dc:creator><![CDATA[Kalin Dimitrov]]></dc:creator><pubDate>Thu, 17 Sep 2026 19:49:32 GMT</pubDate><content:encoded><![CDATA[<p>Last July I spent seven pull requests deleting the same component eleven times. Eleven games in my football quiz app had each grown their own search box, and they had drifted apart in the way duplicated code always does: arrow keys behaved four different ways, only four of the eleven kept the mobile keyboard down at the start of a round, several could scroll the page while you were arrowing through results, and one could submit a stale result. The migration replaced all of them with a single shared component, and the final pull request added a test that fails if a twelfth one ever appears.</p>
<p>That gives me something most people writing about AI and code do not have: a repository where I know exactly what the right answer is, and where the wrong answer used to live at a known commit. So I asked a question I had been wondering about since I started letting models write parts of this app. Does the code that already exists decide what the model writes next?</p>
<p>It does, completely.</p>
<h2>The experiment</h2>
<p>The setup is deliberately boring, because the whole point is that only one thing moves. I wrote out a task asking for a footballer search box on a new game screen, told the model to match the conventions of the code it was being shown, and then sent that same text over and over, eight times per model per condition, at a temperature of 0.7 so the runs would not all be carbon copies of each other.</p>
<p>What changes between conditions is which commit of my own repository gets pasted in above the task. For the first I went back to <code>82385a5</code>, where nine per-game search components are sitting there and nothing shared exists yet, and for the second I used <code>cc78828</code>, the commit that closed the migration, where the shared component is present and TeamTies is shown calling it. Both conditions get roughly four hundred lines of genuine source from the repository as it stood, so neither is winning on prompt size, which was the first objection I expected and wanted to rule out before I looked at anything else.</p>
<p>The models were DeepSeek v4 Pro, Llama 4 Maverick, GLM 5.3 and Mistral 3 14B, an odd-looking lineup that has a dull explanation: Claude and GPT both come back 403 on my account's tier, so open weights are what I had to work with.</p>
<table>
<thead>
<tr>
<th>context shown</th>
<th>reused the shared search</th>
<th>hand-rolled a new one</th>
<th>median lines</th>
</tr>
</thead>
<tbody><tr>
<td>the forked era</td>
<td>0%</td>
<td>100%</td>
<td>190</td>
</tr>
<tr>
<td>after the migration</td>
<td>100%</td>
<td>0%</td>
<td>41</td>
</tr>
</tbody></table>
<p>Thirty-two runs each, and not one crossed over. Same model, same instructions, 190 lines of hand-written state machine in one case and a 41-line wrapper in the other, decided entirely by what was lying around in the repository.</p>
<p>I expected a tendency. I did not expect it to be unanimous across four models from four different labs.</p>
<h2>The models reproduce the specific bugs, not just the shape</h2>
<p>The interesting part is not the line count, it is what is in those 190 lines. The migration existed because eleven copies had drifted on specific behaviours, and the test I added lists them. So I checked the generated components against that same list.</p>
<p>Of the 33 hand-rolled components the old context produced:</p>
<ul>
<li><strong>0%</strong> made the arrow keys wrap around at the ends of the list, which is the first behaviour the drift guard names.</li>
<li><strong>0%</strong> used <code>SEARCH_MIN_QUERY_LENGTH</code>, and <strong>97%</strong> re-typed a bare <code>&gt;= 2</code> instead — the exact thing the repository's own documentation tells you never to do.</li>
<li><strong>58%</strong> had any mobile focus guard at all, against a rule that says the keyboard must not cover the board until the player has had a go.</li>
</ul>
<p>These are not generic AI mistakes. They are my team's mistakes, from 2025, faithfully reproduced in 2026 by models that were shown the code containing them. The forked components taught the models to fork, and taught them the defects too.</p>
<p>There is a cleaner way to put the damage. Every one of the 32 components generated from the old context calls <code>useFootballerSearch</code>. That hook was deleted in the final pull request of the migration. All 32 would fail to compile against the repository as it stands today, which is a strange thing to say about code that perfectly matches the codebase it was shown.</p>
<h2>So what actually steers it</h2>
<p>If old code drags the model backwards, the practical question is what you can do about it when your repository is full of old code. I ran two more conditions to find out.</p>
<p>In the first, the model sees only the forked components, exactly as before, plus one paragraph from the repository's <code>CLAUDE.md</code>: the rule saying every search goes through the shared utility and never hand-roll another typeahead. In the second, the model sees no source for the shared component at all, only its filename in a directory listing.</p>
<table>
<thead>
<tr>
<th>context shown</th>
<th>reached for the shared search</th>
<th>hand-rolled</th>
</tr>
</thead>
<tbody><tr>
<td>forked code only</td>
<td>0%</td>
<td>100%</td>
</tr>
<tr>
<td>forked code + the written rule</td>
<td>100%</td>
<td>0%</td>
</tr>
<tr>
<td>just the filename in a listing</td>
<td>97%</td>
<td>3%</td>
</tr>
<tr>
<td>the shared component's source</td>
<td>100%</td>
<td>0%</td>
</tr>
</tbody></table>
<p>One paragraph of documentation, sitting next to nine hundred lines of exactly the code it forbids, flipped every single run. That is a better return than I expected from a file most people assume gets skimmed.</p>
<h2>Except the documentation does not finish the job</h2>
<p>Here is where the neat story breaks, and it took a second measurement to see it.</p>
<p>Reaching for the shared search only means the model wrote the component's name into its JSX. It does not mean it called it correctly. The component has 23 real props, so I compared the props in each generated call against that list.</p>
<p>When the model could see the component's source, it invented nothing: none of those 32 calls used a prop that does not exist. When it had only the written rule, <strong>28.6%</strong> of calls invented at least one. When it had only the filename, <strong>82.1%</strong> did, averaging 4.64 imaginary props per call.</p>
<p>So the file listing result is largely a mirage. Telling a model that the component exists reliably stops it hand-rolling a typeahead, and then it writes a confident call to an API it has never seen, with <code>onFootballerSelect</code> and <code>debounceMs</code> and other things I never wrote. The 97% reuse figure is nominal; most of that code does not compile.</p>
<p>The rule and the source do different jobs. The rule decides which road it takes. The source decides whether the code at the end of the road is real.</p>
<h2>One thing that did not go wrong</h2>
<p>I expected a single leftover fork to poison the well, so I ran a condition with the shared component present <em>and</em> one of the old forked files still sitting in view. It made no difference: every run still reused the shared component, at essentially the same length.</p>
<p>That is worth saying plainly because it contradicts the tidy version of this post. Models are not dragged down by the worst code in the repository, they follow the dominant pattern. You do not have to sweep up every last bad example before your AI-assisted work improves. You have to make the good abstraction exist, and make it visible.</p>
<h2>What I got wrong</h2>
<p>What I got wrong was the prop-counting instrument, and I nearly published its output.</p>
<p>My first pass reported that every call in <em>every</em> condition invented props that do not exist, including the condition where the model could read the component's source. I believed it for about a minute, because it flattered a story I was already enjoying. Then it occurred to me that a model with the type definition in front of it inventing nine imaginary props on every single run is not a plausible thing to happen.</p>
<p>The regex that was meant to extract the real prop names had matched nothing, so the set of real props was empty and every prop the models used counted as invented. That result was measuring my own broken parser. With the list actually populated, the true figure for that condition is zero, and the real finding — that invention rises sharply as context thins — only appears once the instrument works.</p>
<p>It is the same lesson as the last few of these: when a number arrives that is both extreme and convenient, the first suspect is the thing doing the measuring.</p>
<h2>What to take from it</h2>
<p>If you are letting a model write code in an existing repository, the repository is your prompt, whether you intended it or not. Everything in these results points the same way. The model is not consulting best practice, it is pattern-matching on what is in front of it, and it will reproduce a pattern you abandoned a year ago with complete confidence and a hook that no longer exists.</p>
<p>Three things follow, in the order they pay off. Delete the old pattern, because as long as it is the only thing in the repo it is the specification. Write the rule down where the tools will read it, since that one paragraph was worth more than every line of example code around it. And make sure the real implementation, not just its name, is reachable, because a model that knows your component exists but cannot see it will invent its interface without hesitating.</p>
<p>The migration was worth doing for the humans. The measurements say it changed what the machines write too, by a wider margin than it changed what we write.</p>
<p><em>Everything above ran on DigitalOcean's inference API against <code>deepseek-v4-pro</code>, <code>llama-4-maverick</code>, <code>glm-5.3</code> and <code>mistral-3-14B</code>, 160 completions in total, eight per model per condition.</em></p>
]]></content:encoded></item></channel></rss>