<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Maaz Kazi</title>
    <description>The latest articles on DEV Community by Maaz Kazi (@maazkazi).</description>
    <link>https://dev.to/maazkazi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4140025%2F2e05be23-3327-4840-a1b3-5f95347b7e44.jpg</url>
      <title>DEV Community: Maaz Kazi</title>
      <link>https://dev.to/maazkazi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/maazkazi"/>
    <language>en</language>
    <item>
      <title>Asking a vision model what and where in the same call makes it worse at both</title>
      <dc:creator>Maaz Kazi</dc:creator>
      <pubDate>Fri, 25 Sep 2026 12:23:29 +0000</pubDate>
      <link>https://dev.to/maazkazi/asking-a-vision-model-what-and-where-in-the-same-call-makes-it-worse-at-both-26e9</link>
      <guid>https://dev.to/maazkazi/asking-a-vision-model-what-and-where-in-the-same-call-makes-it-worse-at-both-26e9</guid>
      <description>&lt;p&gt;Handrail takes a screenshot of whatever you are stuck in, answers your question about it, and then draws an arrow on the real control you need to touch. The obvious way to build that is one call: here is the screen, here is the question, give me the answer and the coordinates.&lt;/p&gt;

&lt;p&gt;That is how I built it first, and it is worse at both halves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two passes, one job each
&lt;/h2&gt;

&lt;p&gt;Handrail now makes two vision calls against the same screenshot.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The answering pass.&lt;/strong&gt; The screenshot, the question, the last few turns, and any attached files go to the model. It replies with a short answer, or a checklist when the job genuinely takes several steps, and it names the one control you need to touch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The pointing pass.&lt;/strong&gt; The same screenshot goes back with a single question: where is that control? Answer as coordinates.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Separating them helped for a reason that is obvious in hindsight. The answering pass is a reading and reasoning task over the whole screen. The pointing pass is a localisation task over one named target. Asking for both in one response makes the model hold a spatial answer in working memory while it composes prose, and the coordinates are the part that degrades.&lt;/p&gt;

&lt;p&gt;It also makes failure legible. If the answer is right and the arrow is wrong, you know exactly which pass to fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coordinates that survive the real world
&lt;/h2&gt;

&lt;p&gt;The second pass does not return pixels. It returns a position on a &lt;strong&gt;0-1000 normalised grid&lt;/strong&gt;, which Handrail then maps onto actual screen pixels.&lt;/p&gt;

&lt;p&gt;This sounds like a detail and it removes an entire category of bug. Screens differ in resolution, in DPI scaling, and in how many of them are plugged in. If the model returns pixels, every one of those becomes arithmetic you have to get right on someone else's hardware. Normalised coordinates are resolution-independent by definition, so DPI and multi-monitor never enter the maths at all.&lt;/p&gt;

&lt;p&gt;The two passes also do not get the same image. Answering uses a 1600px JPEG, which is enough to read a screen and cheap to send. Locating uses a native-resolution PNG, because the thing you are pinpointing may be a 12px chevron.&lt;/p&gt;

&lt;h2&gt;
  
  
  The model choice this forced
&lt;/h2&gt;

&lt;p&gt;Splitting the work also changed which model I could use. The default is now the cheapest one that reliably reads a screen. I tried going cheaper still, and the lite tier failed in the most dangerous way available to a screen assistant: it invented menu paths for UI that was not in the screenshot.&lt;/p&gt;

&lt;p&gt;That is the failure mode worth designing against. A model that says it cannot see the control is recoverable. A model that confidently names a menu item that does not exist sends someone hunting through Settings for something that was never there, and the whole product exists to stop exactly that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What stays on the machine
&lt;/h2&gt;

&lt;p&gt;Since the thing is looking at your screen, the trust boundary matters more than the features.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The API key is encrypted at rest by the operating system's own keychain, not by me, and never crosses into the UI layer after setup.&lt;/li&gt;
&lt;li&gt;Conversations and attachments are JSON on disk in the app's data directory. No database, no account, no server.&lt;/li&gt;
&lt;li&gt;Web search is off by default, and the only thing that ever leaves is the screenshot and your question, to the model you chose.&lt;/li&gt;
&lt;li&gt;One production dependency. Everything else is Electron and Node built-ins.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A screen assistant that phones home is a different product, and a worse one.&lt;/p&gt;

&lt;p&gt;Handrail is open source, Apache-2.0, with tagged Windows and macOS releases: &lt;a href="https://github.com/M19K/handrail" rel="noopener noreferrer"&gt;https://github.com/M19K/handrail&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>electron</category>
    </item>
    <item>
      <title>It agreed with the reference 100% of the time. It was right 75% of the time.</title>
      <dc:creator>Maaz Kazi</dc:creator>
      <pubDate>Wed, 23 Sep 2026 20:41:20 +0000</pubDate>
      <link>https://dev.to/maazkazi/it-agreed-with-the-reference-100-of-the-time-it-was-right-75-of-the-time-5c8k</link>
      <guid>https://dev.to/maazkazi/it-agreed-with-the-reference-100-of-the-time-it-was-right-75-of-the-time-5c8k</guid>
      <description>&lt;p&gt;Swap a cheap model in behind an expensive one and the obvious way to check it is shadow traffic: send the same request to both, compare the answers, count how often they agree. High agreement, ship it.&lt;/p&gt;

&lt;p&gt;I measured that on 60 live calls while building SuperRouter. The routed model agreed with the reference &lt;strong&gt;100% of the time&lt;/strong&gt;. It was &lt;strong&gt;correct 75% of the time&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Both numbers are real. The gap between them is the whole problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agreement is not correctness
&lt;/h2&gt;

&lt;p&gt;Agreement asks whether two models produced the same answer. It never asks whether the answer was right. And two models from the same era, trained on overlapping data, fail in the same direction far more often than they fail independently, so agreement is highest exactly where it protects you least.&lt;/p&gt;

&lt;p&gt;A shadow run reporting 100% is consistent with two different worlds: a cheaper model that is genuinely as good, and a cheaper model that is wrong in precisely the same places as the expensive one. Nothing in the agreement number separates them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I do instead
&lt;/h2&gt;

&lt;p&gt;Score against ground truth, per failure mode, and never against the other model.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ground truth is generated, not hand-labelled.&lt;/strong&gt; Faults are planted deliberately, so the right answer is known before any model sees the case.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A planted defect has to move pixels.&lt;/strong&gt; One of 18 fault classes returned success and changed nothing on screen. Every fixture is now gated against a healthy frame of the same screen, because a fault no model could possibly have caught was quietly inflating every score.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Both halves of the exam have to be hard.&lt;/strong&gt; The faithful cases were verbatim copies of the source, so false-alarm rates sat at 0-3% across seven models and that axis measured nothing at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runs from different versions of a set are never compared.&lt;/strong&gt; Every run fingerprints the exact cases it sat. Without that, the table ranked a model measured on 90 easy cases above one measured on 592.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The part that surprised me
&lt;/h2&gt;

&lt;p&gt;I assumed a published leaderboard could stand in for measuring your own product. Across two products, rank order mostly transfers when a model is judging (0.83), but every model dropped a median 22 points in absolute terms. When the task was pointing at the right control rather than judging, the order barely transferred at all (0.49).&lt;/p&gt;

&lt;p&gt;So a leaderboard tells you roughly who is good in general. It does not tell you who is good at your thing, and the second question is the only one that decides your bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;If you are routing to save money, agreement rate is the metric you will reach for first and the one that will mislead you hardest. Measure correctness against something you constructed, split by the ways your product can actually break. Otherwise you are measuring how similar two models are and calling it quality.&lt;/p&gt;

&lt;p&gt;SuperRouter is open source, Apache-2.0, with no runtime dependencies: &lt;a href="https://github.com/M19K/superrouter" rel="noopener noreferrer"&gt;https://github.com/M19K/superrouter&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>testing</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
