<?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: Ivan Stankovic</title>
    <description>The latest articles on DEV Community by Ivan Stankovic (@lognebudo).</description>
    <link>https://dev.to/lognebudo</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%2F3829335%2F909c5a1e-dcd9-4dbd-b691-b976cb0c1bbd.jpg</url>
      <title>DEV Community: Ivan Stankovic</title>
      <link>https://dev.to/lognebudo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lognebudo"/>
    <language>en</language>
    <item>
      <title>Four numbers Ollama 0.40 gives you now, and which ones to trust</title>
      <dc:creator>Ivan Stankovic</dc:creator>
      <pubDate>Sun, 11 Oct 2026 21:25:40 +0000</pubDate>
      <link>https://dev.to/lognebudo/four-numbers-ollama-040-gives-you-now-and-which-ones-to-trust-3m80</link>
      <guid>https://dev.to/lognebudo/four-numbers-ollama-040-gives-you-now-and-which-ones-to-trust-3m80</guid>
      <description>&lt;p&gt;Ollama went from 0.33 to 0.40 in six weeks, and most of what changed is invisible unless you&lt;br&gt;
read the responses with a magnifier. I did, because I maintain a tool that has to. Here are&lt;br&gt;
four numbers the daemon reports now that it didn't before, what each one actually means, and&lt;br&gt;
in one case why you should not believe it.&lt;/p&gt;

&lt;p&gt;Everything below is two &lt;code&gt;curl&lt;/code&gt; calls on your own machine. The tool is at the end.&lt;/p&gt;
&lt;h2&gt;
  
  
  1. A prefill rate the server computes for you — wrongly
&lt;/h2&gt;

&lt;p&gt;Since 0.35.1 the OpenAI-compatible endpoint attaches a &lt;code&gt;timings&lt;/code&gt; block to the final usage&lt;br&gt;
chunk. Send one prompt twice, read the second answer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"usage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"prompt_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"prompt_tokens_details"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"cached_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"completion_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"timings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"prompt_n"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"prompt_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"prompt_per_second"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3875&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"predicted_n"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"predicted_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;83&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"predicted_per_second"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;132.5&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffvl5zl269vw0k8pvgonq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffvl5zl269vw0k8pvgonq.png" alt=" " width="800" height="634"&gt;&lt;/a&gt;&lt;br&gt;
One token was evaluated, in 8 milliseconds. The block says 3875 tokens per second.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;prompt_n&lt;/code&gt; is the whole prompt, cached tokens included. &lt;code&gt;prompt_ms&lt;/code&gt; is the time spent on the&lt;br&gt;
uncached ones only. Divide the first by the second and you get a rate inflated by exactly the&lt;br&gt;
cache hit ratio: &lt;code&gt;count / (count - cached)&lt;/code&gt;, so 31x here, and anything up to a few hundred on a&lt;br&gt;
long system prompt that matched. The honest figure sits in the same chunk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(prompt_tokens - cached_tokens) / prompt_ms  =  1 / 8 ms  =  125 tok/s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that looks familiar, it is the formula bug that the native API acquired in 0.33.3, when&lt;br&gt;
&lt;code&gt;prompt_eval_duration&lt;/code&gt; started timing only uncached tokens while &lt;code&gt;prompt_eval_count&lt;/code&gt; stayed the&lt;br&gt;
total. Now the server emits the result of that division itself, labelled as a rate. The decode&lt;br&gt;
half, &lt;code&gt;predicted_per_second&lt;/code&gt;, is fine; generated tokens are never cached.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What LLMxRay does with it:&lt;/strong&gt; the Protocol Observatory's envelope diff now has three rows for&lt;br&gt;
prefill: the time, the rate as the daemon states it, and the rate over evaluated tokens. Run a&lt;br&gt;
prompt twice and the stated cell turns amber on the second run. That is the whole feature; the&lt;br&gt;
point is to see the two numbers diverge on your own daemon.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. A probability instead of a "yes"
&lt;/h2&gt;

&lt;p&gt;0.35.1 added &lt;strong&gt;decision models&lt;/strong&gt;: &lt;code&gt;nimble&lt;/code&gt;, &lt;code&gt;tev1&lt;/code&gt;, &lt;code&gt;clef&lt;/code&gt;, &lt;code&gt;clef-flash&lt;/code&gt;. They do not write text.&lt;br&gt;
You give &lt;code&gt;/v1/systemone&lt;/code&gt; a state and typed questions, and you get back the model's own&lt;br&gt;
distribution over the candidates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl localhost:11434/v1/systemone &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
  "model": "nimble",
  "state": "Our checkout has returned 500 errors since 9am. Customers cannot pay.",
  "questions": {
    "label":    {"type": "choice", "instructions": "Which label fits this ticket?",
                 "criteria": {"billing": "Payments and refunds", "bug": "Software errors", "account": "Login and account access"}},
    "urgent":   {"type": "noul",   "instructions": "Is this ticket urgent?"},
    "severity": {"type": "score",  "instructions": "How severe is the impact?",
                 "criteria": ["cosmetic", "minor", "major", "full outage"]}
  }}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"choice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"choice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bug"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"probabilities"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"billing"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.038&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"bug"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.907&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"account"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.055&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.66&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"urgent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"noul"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"noul"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.9897&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"score"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2.37&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"probabilities"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"0"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.005&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.022&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"2"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.569&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"3"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.403&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things worth knowing before you build on it. &lt;code&gt;score&lt;/code&gt; criteria must be an array; a map is&lt;br&gt;
rejected with a clear message. Limits are 2 to 26 candidates and 1 to 64 questions per request.&lt;br&gt;
And a &lt;code&gt;score&lt;/code&gt; is the expected position on your scale, so 2.37 on a four-step scale means&lt;br&gt;
"major, leaning to full outage", which is more information than either label alone.&lt;/p&gt;

&lt;p&gt;The trap: these models report &lt;code&gt;decision&lt;/code&gt; as their &lt;strong&gt;only&lt;/strong&gt; capability, and &lt;code&gt;/api/chat&lt;/code&gt; does not&lt;br&gt;
refuse them. Ask &lt;code&gt;nimble&lt;/code&gt; "hi" and it streams a reply about the schema it was not given. Any&lt;br&gt;
client that builds its model list from &lt;code&gt;/api/tags&lt;/code&gt; without reading &lt;code&gt;capabilities&lt;/code&gt; will offer a&lt;br&gt;
decision model for chat.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What LLMxRay does with it:&lt;/strong&gt; decision models are kept out of the chat and embedding selectors&lt;br&gt;
by capability, and a Decision Lab page sends the request above and draws each answer as&lt;br&gt;
probability bars with the chosen candidate highlighted, plus the token usage. The sample on the&lt;br&gt;
page is the ticket above, asked three ways.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. Which runtime actually ran your model
&lt;/h2&gt;

&lt;p&gt;0.40 made MLX the default on Apple Silicon for the architectures it supports, which means the&lt;br&gt;
same model tag can now run on two different engines. The daemon says which:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;/api/tags&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"details"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"runner"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mlx"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"llamacpp"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ggml"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;models&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;pulled&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;before&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.40&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;/api/ps&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="err"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"runner"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mlx"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the Mac mini I measured with, &lt;code&gt;nomic-embed-text&lt;/code&gt; sat on &lt;code&gt;llamacpp&lt;/code&gt; and &lt;code&gt;embeddinggemma-2&lt;/code&gt; on&lt;br&gt;
&lt;code&gt;mlx&lt;/code&gt; at the same time. A benchmark result that only records the model name is now ambiguous on&lt;br&gt;
that machine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What LLMxRay does with it:&lt;/strong&gt; the runner is shown on each model and each running model, and&lt;br&gt;
every new benchmark result is stamped with it, so two results under one name are only compared&lt;br&gt;
when the runner matches. Results recorded before 0.7.0 carry no runner, and the page says so&lt;br&gt;
rather than guessing.&lt;/p&gt;
&lt;h2&gt;
  
  
  4. An image costs 258 tokens to embed, on one platform
&lt;/h2&gt;

&lt;p&gt;Also in 0.40: &lt;code&gt;/api/embed&lt;/code&gt; accepts an image next to the text, on models that report both&lt;br&gt;
&lt;code&gt;embedding&lt;/code&gt; and &lt;code&gt;vision&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl localhost:11434/api/embed &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"model": "embeddinggemma-2",
  "input": {"text": "a cat on a windowsill", "image": "&amp;lt;base64 png&amp;gt;"}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;&lt;code&gt;prompt_eval_count&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Vector&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;short sentence&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;768 dims&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;same sentence + a 1x1 PNG&lt;/td&gt;
&lt;td&gt;265&lt;/td&gt;
&lt;td&gt;768 dims&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A one-pixel image pays a ~258-token block, so the cost is per image, not per pixel. Sending an&lt;br&gt;
image to a text-only embedding model returns HTTP 501, &lt;code&gt;model does not support media&lt;br&gt;
embeddings&lt;/code&gt;, a status worth matching on rather than a generic 400.&lt;/p&gt;

&lt;p&gt;The catch is in the model, not the API: &lt;code&gt;embeddinggemma-2&lt;/code&gt; is the only multimodal embedding&lt;br&gt;
model in the library, and it requires MLX. On Windows the pull fails with "this model requires&lt;br&gt;
MLX support". So in October 2026, image embeddings in Ollama are an Apple Silicon feature. I&lt;br&gt;
measured the table above by forwarding a Mac mini's daemon over SSH.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What LLMxRay does with it:&lt;/strong&gt; on the Embeddings page, when the selected model reports embedding&lt;br&gt;
and vision, an image can be attached in the playground and on both sides of the similarity&lt;br&gt;
calculator: photo against sentence, or photo against photo. Every result shows the prompt tokens&lt;br&gt;
it cost. Text-only models show no image controls at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  One more thing the daemon does quietly
&lt;/h2&gt;

&lt;p&gt;0.40.2 upgrades models in the background the first time you run them and keeps the original&lt;br&gt;
copy on disk, so a downgrade stays safe. The release notes give a &lt;code&gt;jq&lt;/code&gt; pipeline to find and&lt;br&gt;
delete the backups by digest. LLMxRay's Models tab does the same selection and offers a remove&lt;br&gt;
button per backup. In fairness: no daemon I have access to has produced a backup yet, 0.40.3&lt;br&gt;
paused the upgrades, so that part is built from Ollama's own recipe and tested against its shape,&lt;br&gt;
not against a live backup. The section stays hidden until one exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tool
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/LogneBudo/llmxray" rel="noopener noreferrer"&gt;LLMxRay&lt;/a&gt; is a local observability UI for Ollama:&lt;br&gt;
Apache-2.0, nothing leaves localhost, &lt;code&gt;npx llmxray&lt;/code&gt; or &lt;code&gt;docker pull djovaneli/llmxray&lt;/code&gt;. Version&lt;br&gt;
0.7.1 is the one with everything above, verified against Ollama 0.40.3. The guides are in&lt;br&gt;
English, French and Chinese.&lt;/p&gt;

&lt;p&gt;None of the four numbers needs it. What it adds is the habit of putting the daemon's figure and&lt;br&gt;
the honest figure on the same screen, which is how the first one was caught.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>observability</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Ollama says my model does 13,826 tokens/sec. It does 43.</title>
      <dc:creator>Ivan Stankovic</dc:creator>
      <pubDate>Tue, 15 Sep 2026 22:14:04 +0000</pubDate>
      <link>https://dev.to/lognebudo/ollama-says-my-model-does-13826-tokenssec-it-does-43-54g</link>
      <guid>https://dev.to/lognebudo/ollama-says-my-model-does-13826-tokenssec-it-does-43-54g</guid>
      <description>&lt;p&gt;That number is not a typo, and my GPU has not improved. Both figures came out of the same daemon, from the same prompt, thirty seconds apart.&lt;/p&gt;

&lt;p&gt;If you have Ollama running, you can check this on your own machine before you read any further. Send the same prompt twice and print three fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qwen2.5:7b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;options&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;num_predict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;temperature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:11434/api/chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                 &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a helpful assistant. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Context paragraph about nothing. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Say OK.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cold&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;warm&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt_eval_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt_eval_cached_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;seconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt_eval_duration&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;1e9&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; count=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; cached=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
          &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rate=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;,.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; tok/s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is what I get. Ollama 0.34.0, qwen2.5:7b, one machine, nothing else running:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;prompt_eval_count&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;prompt_eval_cached_count&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;prompt_eval_duration&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;count ÷ duration&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;cold&lt;/td&gt;
&lt;td&gt;318&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;139.97 ms&lt;/td&gt;
&lt;td&gt;2,272 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;warm&lt;/td&gt;
&lt;td&gt;318&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;317&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;23.00 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;13,826 tok/s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The second row claims the model prefilled 318 tokens at nearly fourteen thousand tokens per second. It did not. It prefilled &lt;strong&gt;one&lt;/strong&gt; token and read the other 317 out of the KV cache. The honest prefill rate for that row is 43 tok/s.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the number comes out wrong
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;prompt_eval_duration&lt;/code&gt; times only the tokens that were &lt;strong&gt;actually computed&lt;/strong&gt;.&lt;br&gt;
&lt;code&gt;prompt_eval_count&lt;/code&gt; still reports the &lt;strong&gt;total&lt;/strong&gt; prompt size, cached tokens included. So the obvious calculation divides a whole prompt by the time taken to process a fraction of it, and the error is not a small constant — it is exactly the cache hit ratio:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;inflation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prompt_eval_count&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt_eval_count&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;prompt_eval_cached_count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With 317 of 318 tokens cached, that is 318x. Note what this means in practice: &lt;strong&gt;the better your cache is working, the more the number lies.&lt;/strong&gt; A cold prompt reports truthfully. A perfectly warm one is off by the length of your system prompt. Any dashboard plotting prefill tokens/sec over a conversation is drawing a curve of its own cache hit rate and labelling it throughput.&lt;/p&gt;

&lt;p&gt;The fix is one term:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;uncached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;uncached&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;seconds&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;uncached&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is also what Ollama's own &lt;code&gt;Metrics.Summary()&lt;/code&gt; does, so the daemon is not really disagreeing with itself — it just exposes two fields whose units stopped matching, and the older, more obvious one is the one everybody already had in their code.&lt;/p&gt;

&lt;p&gt;Two details worth having:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;When &lt;code&gt;uncached&lt;/code&gt; is zero, report nothing.&lt;/strong&gt; A fully cached prompt has no prefill rate. Print an em dash, not &lt;code&gt;0&lt;/code&gt;, and not &lt;code&gt;∞&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Absent is not zero.&lt;/strong&gt; Daemons before 0.33.3 omit &lt;code&gt;prompt_eval_cached_count&lt;/code&gt; entirely.
Defaulting a missing field to &lt;code&gt;0&lt;/code&gt; silently turns "I don't know" into a confident,   wrong claim that nothing was cached. Leave it undefined and say so in the UI.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The same fact under three different names
&lt;/h2&gt;

&lt;p&gt;Ollama exposes this through three API surfaces, and each calls it something else. All three verified on 0.34.0, same prompt, same warm cache:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;endpoint&lt;/th&gt;
&lt;th&gt;field&lt;/th&gt;
&lt;th&gt;value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;/api/chat&lt;/code&gt; (native)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;prompt_eval_cached_count&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;317&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;/v1/chat/completions&lt;/code&gt; (OpenAI-compatible)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;usage.prompt_tokens_details.cached_tokens&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;317&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;/v1/messages&lt;/code&gt; (Anthropic-compatible)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;usage.cache_read_input_tokens&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;317&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One trap on that last row. The Anthropic-compatible endpoint returned&lt;br&gt;
&lt;code&gt;input_tokens: 1&lt;/code&gt; for a 318-token prompt. &lt;code&gt;input_tokens&lt;/code&gt; there means &lt;em&gt;total minus cache reads&lt;/em&gt;, not the total — so reconstructing prompt size means adding the two together. If you are summing &lt;code&gt;input_tokens&lt;/code&gt; across turns to estimate load, warm turns will quietly contribute almost nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now that the cache is visible, it is worth measuring
&lt;/h2&gt;

&lt;p&gt;The useful consequence of a readable &lt;code&gt;cached_count&lt;/code&gt; is that you can finally see something most of us only assert: a local model reuses its KV cache only while the prompt matches &lt;strong&gt;from the very first token&lt;/strong&gt;. One volatile value near the top forfeits everything after it, every single turn.&lt;/p&gt;

&lt;p&gt;Same body text, 227 tokens, one timestamp. Turn one primes the cache; turn two sends the same layout with a &lt;em&gt;fresh&lt;/em&gt; timestamp, which is what real traffic does:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;timestamp position&lt;/th&gt;
&lt;th&gt;tokens&lt;/th&gt;
&lt;th&gt;reused&lt;/th&gt;
&lt;th&gt;prefill&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;front&lt;/td&gt;
&lt;td&gt;227&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;41.2 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;back&lt;/td&gt;
&lt;td&gt;227&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;211&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;16.3 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Identical words, identical model, 2.5x the prefill time, purely from where the changing value sits. The 40 tokens still reused in the front case are the chat template's own stable preamble, not your content — everything you wrote is recomputed.&lt;/p&gt;

&lt;p&gt;So the rule is boring and worth following anyway: &lt;strong&gt;stable text first, volatile text last.&lt;/strong&gt; Current date, session id, user name, retrieved chunks that change per turn — push them to the end of the prompt, below anything that stays put.&lt;/p&gt;

&lt;h2&gt;
  
  
  The measurement I got wrong first
&lt;/h2&gt;

&lt;p&gt;I want to flag this, because I spent a day producing a confidently inverted result.&lt;/p&gt;

&lt;p&gt;My first attempt asked: &lt;em&gt;how much of layout A's cache does layout B reuse?&lt;/em&gt; I primed with one layout, sent the other, and read the hit count. It said the original prompt was getting 323 of 324 tokens cached and my improved rewrite was getting 3 — that the fix made things four times worse.&lt;/p&gt;

&lt;p&gt;The question was wrong. Nobody alternates between two prompt layouts. Real traffic sends &lt;strong&gt;the same layout every turn with a fresh value in it&lt;/strong&gt;, so each layout has to be measured against &lt;em&gt;itself&lt;/em&gt;: send it twice, mutate the volatile value between the sends, and take the &lt;strong&gt;second&lt;/strong&gt; send as the measurement. Measuring the first send only tells you how well a prompt matches itself, which is ~100% for every layout ever written and tells you nothing.&lt;/p&gt;

&lt;p&gt;The thing I keep taking from this: the bug was not in the arithmetic, and no unit test would have caught it, because the code did exactly what I asked. Only the live daemon could tell me I was asking the wrong question. If you are measuring cache behaviour, prime it and mutate it — a single cold send measures nothing you care about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Go and check your own numbers
&lt;/h2&gt;

&lt;p&gt;The two scripts above are the whole point of this post; everything else is commentary on what they print. If you are running an observability dashboard, a benchmark harness, or anything that reports prefill tokens/sec for a local model, it is worth thirty seconds to find out whether it divides by &lt;code&gt;prompt_eval_count&lt;/code&gt; or by &lt;code&gt;prompt_eval_count - prompt_eval_cached_count&lt;/code&gt;. Mine divided by the wrong one for months.&lt;/p&gt;

&lt;p&gt;Numbers here are from a single machine (Ollama 0.34.0, qwen2.5:7b, 2026-09-16) and yours will differ — the inflation factor in particular is just your cache hit ratio, so it scales with how long your system prompt is. The direction of the effect is not machine-specific.&lt;/p&gt;

&lt;p&gt;I maintain &lt;a href="https://github.com/LogneBudo/llmxray" rel="noopener noreferrer"&gt;LLMxRay&lt;/a&gt;, a local LLM observatory that now surfaces the cached counts across all three protocols and has a page for running the prime-and-mutate test against your own daemon. It is Apache-2.0, local-only, and there is nothing to sign up for. But the fields are in the API whether or not you use anything of mine, and&lt;br&gt;
the one-term fix above is the part that actually matters.&lt;/p&gt;

</description>
      <category>ollama</category>
      <category>llm</category>
      <category>performance</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Stop Guessing, Start Seeing: Multi-Model Observability with LLMxRay 🕵️‍♂️</title>
      <dc:creator>Ivan Stankovic</dc:creator>
      <pubDate>Fri, 03 Apr 2026 20:55:02 +0000</pubDate>
      <link>https://dev.to/lognebudo/stop-guessing-start-seeing-multi-model-observability-with-llmxray-1djh</link>
      <guid>https://dev.to/lognebudo/stop-guessing-start-seeing-multi-model-observability-with-llmxray-1djh</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmu8knu7mylfhykwoe7ty.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmu8knu7mylfhykwoe7ty.png" alt=" " width="800" height="442"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Have you ever wondered why the same prompt costs more in one language than another? Or why a model feels "smarter" in English but struggles with Arabic or Chinese?&lt;/p&gt;

&lt;p&gt;When working with LLMs, we often treat the response as a black box. We see the output, but we don't see the mechanics—the tokenization, the side-by-side comparison of different model families, or how different writing systems affect performance.&lt;/p&gt;

&lt;p&gt;I built LLMxRay to pull back the curtain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is LLMxRay?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;LLMxRay is an open-source observability tool designed to help developers inspect how different LLMs handle the exact same prompt in real-time. Whether you are using local models via Ollama/LM Studio or cloud-based APIs, LLMxRay gives you a "side-by-side" X-ray view of your prompt's journey.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why use it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Multi-Model Comparison: Run one prompt against multiple models simultaneously. See how Llama 3 compares to Mistral or GPT-4o in one view.&lt;/p&gt;

&lt;p&gt;Multilingual Deep-Dive: This was a big focus for me. The tool supports 4 languages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;English 🇺🇸&lt;/li&gt;
&lt;li&gt;French 🇫🇷&lt;/li&gt;
&lt;li&gt;Arabic 🇸🇦 (RTL support)&lt;/li&gt;
&lt;li&gt;Chinese 🇨🇳&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tokenization Transparency: See exactly how your text is being chopped up into tokens. This is crucial for debugging cost, context window limits, and model "reasoning" quality across different writing systems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why 4 Languages?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Tokenization isn't equal. A single concept might be 1 token in English but 3 tokens in another language. By supporting Latin, RTL (Arabic), and character-based (Chinese) scripts, LLMxRay lets you see the economic and technical difference of running multilingual apps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it out&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The project is early-stage and open for feedback! You can connect it to your local environment or use your API keys to start comparing models immediately.&lt;/p&gt;

&lt;p&gt;👉 Check out the repo here: &lt;br&gt;
&lt;a href="https://github.com/LogneBudo/llmxray" rel="noopener noreferrer"&gt;https://github.com/LogneBudo/llmxray&lt;/a&gt;&lt;br&gt;
or website and docs here:&lt;br&gt;
&lt;a href="https://lognebudo.github.io/llmxray/" rel="noopener noreferrer"&gt;https://lognebudo.github.io/llmxray/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I’d love to hear from the DEV community:&lt;/p&gt;

&lt;p&gt;Which model families do you want to see compared next?&lt;/p&gt;

&lt;p&gt;Are there specific visualizations that would help your LLM workflow?&lt;/p&gt;

&lt;p&gt;Drop a comment below or open an issue on GitHub! 🚀&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>LLMxRay: A Local Observatory for Understanding How LLMs Think</title>
      <dc:creator>Ivan Stankovic</dc:creator>
      <pubDate>Tue, 17 Mar 2026 11:47:10 +0000</pubDate>
      <link>https://dev.to/lognebudo/llmxray-a-local-observatory-for-understanding-how-llms-think-4pj8</link>
      <guid>https://dev.to/lognebudo/llmxray-a-local-observatory-for-understanding-how-llms-think-4pj8</guid>
      <description>&lt;p&gt;Modern LLMs generate impressive results, but the most interesting part isn’t the final answer — it’s everything that happens before the answer appears.&lt;br&gt;
Token probabilities, confidence shifts, reasoning traces, tool calls, divergences between models… all of this is usually hidden.&lt;br&gt;
I wanted a way to see these internals clearly, locally, and without relying on cloud APIs.&lt;br&gt;
That’s how LLMxRay started.&lt;/p&gt;

&lt;p&gt;🧠 What LLMxRay does&lt;br&gt;
LLMxRay is a local-first observability tool for LLMs.&lt;br&gt;
It works with Ollama, LM Studio, llama.cpp, and any endpoint that streams tokens.&lt;br&gt;
It gives you a real-time view of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Token-by-token generation with confidence heatmaps&lt;/li&gt;
&lt;li&gt;Reasoning traces (when the model exposes them)&lt;/li&gt;
&lt;li&gt;Side-by-side model comparison&lt;/li&gt;
&lt;li&gt;Tool/function call execution&lt;/li&gt;
&lt;li&gt;Latency and cost breakdowns&lt;/li&gt;
&lt;li&gt;Agent behavior introspection&lt;/li&gt;
&lt;li&gt;A built-in Tools Workshop to design and test function-calling &lt;/li&gt;
&lt;li&gt;flows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything runs locally.&lt;br&gt;
No cloud, no telemetry, no accounts.&lt;/p&gt;

&lt;p&gt;🔍 Why I built it&lt;/p&gt;

&lt;p&gt;Working with local models, I often needed to answer questions like:&lt;br&gt;
• Why did this model choose this token?&lt;br&gt;
• Where did the reasoning diverge?&lt;br&gt;
• Why does Q4_K_M behave differently from Q6_K?&lt;br&gt;
• What exactly happened during a tool call?&lt;br&gt;
• How do two models respond to the same prompt internally?&lt;/p&gt;

&lt;p&gt;Existing UIs focus on chat experience, not introspection.&lt;br&gt;
Debugging required custom scripts, logs, or guesswork.&lt;br&gt;
LLMxRay tries to make this transparent.&lt;/p&gt;

&lt;p&gt;🛠️ How it works&lt;/p&gt;

&lt;p&gt;LLMxRay sits between you and your local model:&lt;/p&gt;

&lt;p&gt;• It captures the token stream&lt;br&gt;
• It records probabilities and reasoning (if available)&lt;br&gt;
• It visualizes everything in a clean, interactive UI&lt;br&gt;
• It stores traces so you can compare runs&lt;br&gt;
• It supports multiple models and endpoints&lt;br&gt;
You can run it with:&lt;/p&gt;

&lt;p&gt;Or clone the repo and run it locally.&lt;/p&gt;

&lt;p&gt;📦 Links&lt;/p&gt;

&lt;p&gt;• GitHub: &lt;a href="https://github.com/lognebudo/llmxray" rel="noopener noreferrer"&gt;https://github.com/lognebudo/llmxray&lt;/a&gt;&lt;br&gt;
• Demo / docs: &lt;a href="https://lognebudo.github.io/llmxray/" rel="noopener noreferrer"&gt;https://lognebudo.github.io/llmxray/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;💬 What’s next&lt;br&gt;
I’m working on:&lt;/p&gt;

&lt;p&gt;• better comparison tools&lt;br&gt;
• an education pillar with kits for teachers and students&lt;br&gt;
• improved reasoning visualization&lt;br&gt;
• support for more local runtimes&lt;/p&gt;

&lt;p&gt;If you work with local models, I’d love to hear how you debug or introspect them — and what features would help you the most.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>learning</category>
      <category>news</category>
    </item>
  </channel>
</rss>
