<?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: Aman</title>
    <description>The latest articles on DEV Community by Aman (@onlyoneaman).</description>
    <link>https://dev.to/onlyoneaman</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%2F250698%2Fd899494b-6757-481e-98a5-3fa7619bf708.jpeg</url>
      <title>DEV Community: Aman</title>
      <link>https://dev.to/onlyoneaman</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/onlyoneaman"/>
    <language>en</language>
    <item>
      <title>Testing Jev on public and private data: classifier or filter?</title>
      <dc:creator>Aman</dc:creator>
      <pubDate>Fri, 18 Sep 2026 07:58:00 +0000</pubDate>
      <link>https://dev.to/onlyoneaman/testing-jev-on-public-and-private-data-classifier-or-filter-31pc</link>
      <guid>https://dev.to/onlyoneaman/testing-jev-on-public-and-private-data-classifier-or-filter-31pc</guid>
      <description>&lt;p&gt;&lt;a href="https://typesafe.ai" rel="noopener noreferrer"&gt;TypeSafe&lt;/a&gt; opened early access to &lt;a href="https://typesafe.ai/blog/introducing-system-one-models-and-jev" rel="noopener noreferrer"&gt;Jev&lt;/a&gt; on 15 September. The headline numbers in the launch compare it with a frontier model writing text. Most teams do not run classification on a frontier model. The decisions a pipeline makes thousands of times a day (is this page worth reading, is this email a request, which bucket does this go in) mostly run on the small, fast tier already: flash-lite class models, gpt-5.4-mini, gpt-5.6-luna. That is the tier Jev has to beat, and the one I measured it against.&lt;/p&gt;

&lt;p&gt;Jev is a model that does not write text. You give it some text and a question with fixed answers, and it gives back a probability for each answer. One call looks like this:&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;POST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;https://api.typesafe.ai/v&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;/systemone&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"jev-latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"state"&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;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"i am sorry that i was unable to get the full brunt of the comedy ."&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"questions"&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="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sentiment"&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="w"&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;"instructions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Is this movie review positive or negative?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"criteria"&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;"negative"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Unfavourable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"positive"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Favourable"&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="w"&gt;
  &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="w"&gt;
&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"answers"&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="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sentiment"&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="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;"negative"&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;"negative"&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.97&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"positive"&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.03&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.93&lt;/span&gt;&lt;span class="w"&gt;
    &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="w"&gt;
  &lt;/span&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;"input_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;336&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"jev-1.13.0"&lt;/span&gt;&lt;span class="w"&gt;
&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;That call took 850 ms from my laptop and cost $0.000014: 336 input tokens at $0.042 per million, and output is free because there is none. A &lt;code&gt;choice&lt;/code&gt; can have up to 255 options and comes back with a probability per option plus a &lt;code&gt;confidence&lt;/code&gt; for the pick. A &lt;code&gt;noul&lt;/code&gt; (TypeSafe's name for a yes/no question) comes back as one number, the probability of yes, so 0.05 is a confident no and 0.95 a confident yes. One call can carry many questions. TypeSafe does not say how &lt;code&gt;confidence&lt;/code&gt; is computed; it is not the top probability (0.93 against 0.97 above), but gating on the top probability instead gave the same picture on the public sets, within a point. Confident, in this post, means that number was within 0.1 of either end for a yes/no, or 0.9 or higher for a pick-one.&lt;/p&gt;

&lt;p&gt;I spent two days and about 16,000 calls finding out what it is good for. First on four public classification datasets, with two small OpenAI models answering the same questions. Then on a few thousand real decisions from the pipelines I work on, scored against what actually happened, not against another model's opinion.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;On short-text classification it is level with or ahead of gpt-5.4-mini and gpt-5.6-luna on three of four public sets, at 5 to 56 times lower cost, with a median under a second.&lt;/li&gt;
&lt;li&gt;Its probabilities hold near 0 and 1. On tasks it can do, confident answers were right 90 to 100% of the time; the middle is a coin flip.&lt;/li&gt;
&lt;li&gt;It is best on short input with crisp labels. The longer the input and the fuzzier the labels, or the more business logic the question carries, the more its accuracy and its confidence fall together. On a whole-document read it is worse than the small model we run today, and no prompt fixed that.&lt;/li&gt;
&lt;li&gt;So it is a filter, not a replacement. It takes the calls it is confident about at either end; the rest fall back to the model you already use.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Four public datasets
&lt;/h2&gt;

&lt;p&gt;300 random items from each of &lt;a href="https://huggingface.co/datasets/SetFit/enron_spam" rel="noopener noreferrer"&gt;Enron spam&lt;/a&gt;, &lt;a href="https://huggingface.co/datasets/stanfordnlp/sst2" rel="noopener noreferrer"&gt;SST-2&lt;/a&gt;, &lt;a href="https://huggingface.co/datasets/fancyzhx/ag_news" rel="noopener noreferrer"&gt;AG News&lt;/a&gt; and &lt;a href="https://huggingface.co/datasets/legacy-datasets/banking77" rel="noopener noreferrer"&gt;Banking77&lt;/a&gt;. Every model got the same question and the same one-line description per label, and had to return a probability for every option, because a gate needs that. The two OpenAI models ran through the Responses API with reasoning set to low; gpt-5.6-luna is one of the models on TypeSafe's own eval board.&lt;/p&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%2F1wjdw4cs1i7j1rmzoo6w.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%2F1wjdw4cs1i7j1rmzoo6w.png" alt="Jev's accuracy minus the better LLM, per dataset: ahead by 0.7 to 2.7 points on three sets, behind by 5.7 on Banking77" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;dataset&lt;/th&gt;
&lt;th&gt;Jev&lt;/th&gt;
&lt;th&gt;gpt-5.4-mini&lt;/th&gt;
&lt;th&gt;gpt-5.6-luna&lt;/th&gt;
&lt;th&gt;Jev, accurate when confident (share of items)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Enron spam, 2 options&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;98.7&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;97.7&lt;/td&gt;
&lt;td&gt;98.0&lt;/td&gt;
&lt;td&gt;99.6 (90%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SST-2 sentiment, 2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;95.7&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;92.7&lt;/td&gt;
&lt;td&gt;93.0&lt;/td&gt;
&lt;td&gt;98.8 (82%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AG News topic, 4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;91.3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;88.3&lt;/td&gt;
&lt;td&gt;89.7&lt;/td&gt;
&lt;td&gt;94.7 (88%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Banking77 intent, 77&lt;/td&gt;
&lt;td&gt;76.0&lt;/td&gt;
&lt;td&gt;78.7&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;81.7&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;89.9 (66%)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"When confident" is the pick-one sense: Jev's confidence in its pick was 0.9 or higher. Jev took all 77 Banking77 options in one question without complaint; it is also the one set where both LLMs beat it.&lt;/p&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%2Fro6er0plz1sto30ivglg.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%2Fro6er0plz1sto30ivglg.png" alt="Seconds per call: Jev under one second on every set, the LLMs 1.4 to 5 seconds" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;per call&lt;/th&gt;
&lt;th&gt;Jev&lt;/th&gt;
&lt;th&gt;gpt-5.4-mini&lt;/th&gt;
&lt;th&gt;gpt-5.6-luna&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;median&lt;/td&gt;
&lt;td&gt;0.8-0.9 s&lt;/td&gt;
&lt;td&gt;1.4-4.2 s&lt;/td&gt;
&lt;td&gt;1.4-5.0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;95th percentile&lt;/td&gt;
&lt;td&gt;0.9-1.1 s&lt;/td&gt;
&lt;td&gt;2.1-5.9 s&lt;/td&gt;
&lt;td&gt;2.1-6.8 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;slowest of 300&lt;/td&gt;
&lt;td&gt;1.4-1.8 s&lt;/td&gt;
&lt;td&gt;3.9-16.0 s&lt;/td&gt;
&lt;td&gt;3.6-10.5 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Jev takes about the same time whatever the task. The LLMs get slower as the options grow, because they write a probability for each one. All of this was measured with 20 calls in flight, which is what a pipeline sees. Jev's tail is not always this clean: at 100 calls in flight on our own pages a few per thousand took 10 to 35 seconds.&lt;/p&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%2F3zab3919d501t5uknyb0.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%2F3zab3919d501t5uknyb0.png" alt="One LLM call buys 5 to 56 Jev calls, drawn as dots per dataset" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two notes on cost. The LLM bills include output tokens, and asking for a probability per option is what makes them expensive: on Banking77 that is about 700 output tokens a call, which is why gpt-5.4-mini costs $3.81 per thousand there. Ask for the choice alone and the bill drops severalfold, but then there is no confidence to gate on. And the gap depends on what you compare against: 5 to 15× against gpt-5.6-luna, 100× and more against a model that reasons hard or an agent. More on that below.&lt;/p&gt;

&lt;p&gt;Every case, every per-item answer from all three models, and the scoring code are at &lt;a href="https://github.com/onlyoneaman/jev-eval" rel="noopener noreferrer"&gt;github.com/onlyoneaman/jev-eval&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  On our own pipeline
&lt;/h2&gt;

&lt;p&gt;Public sets are a sanity check. What we cared about was whether Jev could replace any of the classifier work we run today, thousands of small decisions a week. For these the answer key is what happened afterwards: did the page produce anything, did a human keep the task the agent started.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;decision&lt;/th&gt;
&lt;th&gt;items&lt;/th&gt;
&lt;th&gt;agrees with the outcome&lt;/th&gt;
&lt;th&gt;accurate when confident (share of items)&lt;/th&gt;
&lt;th&gt;possible action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;is this page useful for routing a document&lt;/td&gt;
&lt;td&gt;1,005&lt;/td&gt;
&lt;td&gt;97.8%&lt;/td&gt;
&lt;td&gt;100% (63%)&lt;/td&gt;
&lt;td&gt;reject filter: 60% of pages under the line, nothing lost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;is this an administrative page&lt;/td&gt;
&lt;td&gt;1,005&lt;/td&gt;
&lt;td&gt;95.8%&lt;/td&gt;
&lt;td&gt;99.8% (86%)&lt;/td&gt;
&lt;td&gt;reject filter: 52% under the line, nothing lost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;is this a locations schedule page&lt;/td&gt;
&lt;td&gt;1,005&lt;/td&gt;
&lt;td&gt;98.0%&lt;/td&gt;
&lt;td&gt;99.3% (94%)&lt;/td&gt;
&lt;td&gt;reject filter: 25% under the line, nothing lost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;does this page carry a value for one of 135 report rows&lt;/td&gt;
&lt;td&gt;1,005&lt;/td&gt;
&lt;td&gt;68.6%&lt;/td&gt;
&lt;td&gt;78% (57%)&lt;/td&gt;
&lt;td&gt;leave it to the current model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;should this inbound email start a task&lt;/td&gt;
&lt;td&gt;800&lt;/td&gt;
&lt;td&gt;87.4%&lt;/td&gt;
&lt;td&gt;96% (69%)&lt;/td&gt;
&lt;td&gt;filter with a stated loss&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Agrees with the outcome: Jev's yes/no, cutting P(yes) at 0.5, against what happened afterwards (the routing row is checked against today's classifier, since those pages have no outcome of their own). Accurate when confident: accuracy where P(yes) was at or below 0.1 or at or above 0.9, and the share of items in that band, which on pages is mostly the low end.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The simple page types are where it earns its keep. It matched the outcome 96 to 98% of the time, matched our existing classifier 95 to 97%, and its probabilities barely move: the same page asked twice differs by about 0.01. It went in front of the classifier as a reject filter: any page with a P(yes) under the threshold (set as described below) skips the model, everything else goes through as before. Replaying five production runs with the filter off and on gave the same output on all 1,005 pages, with 50 to 67% fewer routing calls, 45 to 74% fewer admin-page calls and 8 to 13% fewer locations calls, the last one smaller because the classifier reads pages in pairs and a pair is only skipped when both pages are under the line.&lt;/p&gt;

&lt;p&gt;The hard page type is where it does not. "Does this page carry a value for one of 135 report rows" means telling apart a mention of something from an actual value for it, and Jev fires on the mention. It needed about twice as many extraction jobs to match our recall, and its probability swung by 0.5 between runs on the same page. A general LLM asked the same question failed the same way, so this is the task, not the model.&lt;/p&gt;

&lt;p&gt;The inbox was the most interesting. Today every mail on a shared service inbox gets an agent session that decides whether it is a request we should work on; that costs about 12 cents and 94 seconds at the median, and most of the time the answer is no. Given the same mail and the same written rules, Jev matched the recorded outcome 87% of the time, and 96% on the mail it was confident about (69% of it). But a few genuine requests come back near zero, a confident no (a routine request that happens to name a third party reads to it like a policy change), so there is no lossless threshold. What it offers is a trade: drop everything under 0.03 and 78% of the non-requests disappear, at the cost of 5 of the 231 real requests in this set. Those five are mostly short forwards from colleagues and mail whose only evidence is a photo, which Jev cannot read; route those two kinds to the agent regardless and what is left loses about one request in 140.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each decision costs us today
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;decision&lt;/th&gt;
&lt;th&gt;today, per 1,000&lt;/th&gt;
&lt;th&gt;Jev call alone, per 1,000&lt;/th&gt;
&lt;th&gt;time per call today&lt;/th&gt;
&lt;th&gt;time per call, Jev&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;triage one inbound email&lt;/td&gt;
&lt;td&gt;$120 (an agent session each)&lt;/td&gt;
&lt;td&gt;15¢, 800× cheaper&lt;/td&gt;
&lt;td&gt;94 s&lt;/td&gt;
&lt;td&gt;~1 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gate one document page for routing&lt;/td&gt;
&lt;td&gt;$1.50 (small LLM)&lt;/td&gt;
&lt;td&gt;11¢, 14×&lt;/td&gt;
&lt;td&gt;2-3 s, more under load&lt;/td&gt;
&lt;td&gt;~1 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gate one page for admin extraction&lt;/td&gt;
&lt;td&gt;$3.50 (small LLM)&lt;/td&gt;
&lt;td&gt;6¢, 58×&lt;/td&gt;
&lt;td&gt;up to 20 s under load&lt;/td&gt;
&lt;td&gt;~1 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On the document pipeline the saving is modest in dollars, a few percent of a run, and large in call volume: about a quarter of all calls to that rate-limited provider go away. On the inbox it is the reverse: few calls, but each one is an agent session, so the dollars are the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trust the ends, not the middle
&lt;/h2&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%2F63pb61h2yts7s5ylipdi.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%2F63pb61h2yts7s5ylipdi.png" alt="Every call as a dot: public calls placed by Jev's confidence and coloured right or wrong, page-gate calls placed by P(yes) and coloured by whether the page mattered; the ends are clean and the middle is mixed" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The useful thing about the number is that the ends mean what they say. On the public sets, 82% of the answers came back with confidence 0.9 or higher and 96% of those were correct; below 0.9 it was right 55 to 72% of the time depending on the band. On our pages the number is P(yes), so the two ends are a confident no and a confident yes, and both hold: 82% of pages came back under 0.1 and 0.3% of those turned out to matter; the 4% at 0.9 and above mattered 97% of the time; the 14% in between run from 6% to 83% depending on the band. No single cut-off works there. So trust the ends, and hand the middle to a model that can actually read the thing. (The page panel pools the three gates, 2,559 calls, because the routing gate contributes only the 549 pages that have an outcome of their own.)&lt;/p&gt;

&lt;p&gt;You do not pick the drop threshold by hand. Take every item you know was a real positive, find the lowest P(yes) Jev gave any of them, and put the line at half of that. Set it on part of your data, check it on the rest, and re-check as new data arrives, because next month can bring a real positive lower than any you have seen. On our gates I set it on four of the five production runs and checked it on the fifth, rotating, and no fold lost a page. The lines came out at 0.12, 0.065 and 0.025 for routing, admin and locations, from 187, 124 and 61 positives, and put 25 to 60% of pages under the line. The line comes from your positives, not from the confident band, so it can land on either side of 0.1. On email it put almost nothing under the line, so there is no useful lossless threshold there, only the trade above.&lt;/p&gt;

&lt;h2&gt;
  
  
  100× cheaper than what?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;compared against&lt;/th&gt;
&lt;th&gt;cost gap&lt;/th&gt;
&lt;th&gt;speed gap&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;gpt-5.6-luna, reasoning low, 2-4 options (measured above)&lt;/td&gt;
&lt;td&gt;5-6×&lt;/td&gt;
&lt;td&gt;1.7-1.9×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gpt-5.6-luna, 77 options (measured above)&lt;/td&gt;
&lt;td&gt;15×&lt;/td&gt;
&lt;td&gt;6×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gpt-5.4-mini, reasoning low (measured above)&lt;/td&gt;
&lt;td&gt;23-56×&lt;/td&gt;
&lt;td&gt;1.7-5×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Terra, with reasoning, on TypeSafe's own board&lt;/td&gt;
&lt;td&gt;~75×&lt;/td&gt;
&lt;td&gt;~25×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a frontier model (Every's test)&lt;/td&gt;
&lt;td&gt;~580×&lt;/td&gt;
&lt;td&gt;~25×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a full agent session doing the same classification, call for call&lt;/td&gt;
&lt;td&gt;~800×&lt;/td&gt;
&lt;td&gt;~100×&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;An LLM's output tokens cost 4 to 8 times its input tokens, and with reasoning on, most of its tokens are thinking. Jev has no output tokens. Keep reasoning low and the answer to a few tokens, and most of the headline gap disappears; what is left is the cheaper input price and a single pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  What others are seeing
&lt;/h2&gt;

&lt;p&gt;Three days in, most of the noise is reaction to the announcement. The &lt;a href="https://news.ycombinator.com/item?id=49717558" rel="noopener noreferrer"&gt;Hacker News thread&lt;/a&gt; is near 500 comments and very few are from people who had called the API. The argument there is about framing: the 193× and 444× headline compares Jev with a frontier model writing its answer out as text; "cannot hallucinate" means the output is typed, not that it is right; and &lt;a href="https://evals.typesafe.ai/" rel="noopener noreferrer"&gt;TypeSafe's own eval board&lt;/a&gt; grades against the average of GPT-6 Astra and Claude Fable 5.1 rather than ground truth, where Jev sits at 67.8% against 74.1% for the best LLM, never top on accuracy, 200× cheaper. When a commenter called it "basically a zero-shot classifier", the &lt;a href="https://news.ycombinator.com/item?id=49718727" rel="noopener noreferrer"&gt;CEO replied "exactly right"&lt;/a&gt;, which is the most useful sentence in the thread. Within two days there were open imitations on GitHub and Hugging Face and a &lt;a href="https://github.com/AnotiaWang/awesome-jev" rel="noopener noreferrer"&gt;70-repo awesome list&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The measured reports, where someone ran Jev against an LLM on the same task, split by the shape of the task.&lt;/p&gt;

&lt;p&gt;Short input, crisp labels: it wins or ties. &lt;a href="https://nearhere.events/blog/typesafe-jev-mistral-gemini-event-validation" rel="noopener noreferrer"&gt;Near Here&lt;/a&gt; ran event listings against Mistral Small 4 and Gemini 3.5 Flash-Lite on high reasoning: 48 of 50 to their 42 and 43, none of the 13 real events rejected, 0.6 s against 2.9 and 3.4 s, 4¢ per thousand against 37¢ and $2.50. Vercel's CTO &lt;a href="https://x.com/cramforce/status/2100269198727602468" rel="noopener noreferrer"&gt;ran it on an existing classifier eval&lt;/a&gt; that used Gemini 2.5 Flash Lite and it "won both on quality (saturated the eval) and speed (6x)". &lt;a href="https://x.com/h_nilforoshan/status/2100411132498641301" rel="noopener noreferrer"&gt;HiringCafe&lt;/a&gt; scores resumes against job descriptions with human labels: Spearman 0.79 at 2¢ per thousand, against 0.77 for DeepSeek V4 Flash at 14¢ and 0.72 for Gemini 3.1 Flash-Lite at 29¢. &lt;a href="https://github.com/bitnovus/jev-spam-eval" rel="noopener noreferrer"&gt;A 19,500-email spam run&lt;/a&gt; got 98.3% zero-shot, level with a logistic regression trained on 15,000 of those emails, and its calibration matches mine: 0.1% spam below 0.1, 99.9% at 0.9 and above, 38% in the 0.5 to 0.6 band. &lt;a href="https://dev.classmethod.jp/en/articles/jev-for-llm-model-routing/" rel="noopener noreferrer"&gt;Classmethod&lt;/a&gt;'s model router: 40 of 40 at 2.5¢ per thousand, lower confidence in the middle tier. &lt;a href="https://every.to/also-true-for-humans/mini-vibe-check-typesafe-s-jev-judged-everything-i-ve-written-in-0-7-seconds" rel="noopener noreferrer"&gt;Every&lt;/a&gt;: 6 of 7 planted defects at 0.35 s a passage, where Claude Fable 5.1 caught 7 at 8.8 s.&lt;/p&gt;

&lt;p&gt;Longer input, fuzzier labels, or a verdict that needs reasoning: it loses, and the fix is the same each time. &lt;a href="https://github.com/anisselbd/jev-phishing-bench" rel="noopener noreferrer"&gt;A 2,000-email phishing bench&lt;/a&gt; had Jev at 62.6% against 81.3% for Claude Haiku 4.5, with worse calibration and a 5-point swing from question wording alone; five narrow yes/no signals plus a logistic regression reached 95%, and Haiku on the same five signals got 93%. &lt;a href="https://github.com/SamuelSacco/jev-exploration" rel="noopener noreferrer"&gt;Samuel Sacco&lt;/a&gt;'s 800-item difficulty ladder found the probabilities overstate the low end and understate the high end, yet everything at 0.9 or above was right, on 21 to 32% of items; his advice, treat the number as a score and fit your own cut-offs on a few hundred labelled cases, is where I landed too. &lt;a href="https://backnotprop.com/blog/jev-poker/" rel="noopener noreferrer"&gt;A poker study&lt;/a&gt; against a solver: 63% on random spots, 94% on easy ones. And TypeSafe's weakest workflow on its own board is invoice processing, 61.8% against 79.1%, the same kind of whole-document read where it failed for me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verdict
&lt;/h2&gt;

&lt;p&gt;Filter. Jev is cheap and fast, answers only yes/no and pick-one questions, and its probability means what it says at the ends. That is enough to put it in front of a classifier and not enough to put it in place of one.&lt;/p&gt;

&lt;p&gt;Below the drop line it was never wrong on our data, so those items skip the classifier and nothing downstream changes; on our page gates that was 25 to 60% of pages. Above 0.9 it is right 97% of the time on our pages and 90 to 99.6% on every public set, so where most items land up there you can act on Jev's answer and never call the classifier at all. We did not need that on pages, where only 3 to 9% land that high, but on a spam gate it is the bigger end: on Enron, 90% of items sit at 0.9 or above with 99.6% accuracy, nine classifier calls in ten gone. Everything in between falls back to the model you already use, exactly as today.&lt;/p&gt;

&lt;p&gt;The pattern held in every test, mine and other people's. Short input, crisp labels: near-perfect and confident. Long input, fuzzy labels, or a rule that needs reasoning: accuracy and confidence fall together, and the fix is to split the question into narrow yes/no signals and combine them in code, which took the phishing bench from 63% to 95%.&lt;/p&gt;

&lt;p&gt;That ceiling is the thing to watch. Everything Jev gets wrong here is a long-input problem, and &lt;a href="https://docs.typesafe.ai/model-jaggedness/jev-1.13" rel="noopener noreferrer"&gt;TypeSafe's own notes&lt;/a&gt; say accuracy falls as the state grows. If a later version holds up on a full page or a full thread the way this one holds up on a sentence, the filter becomes the classifier, even for the complex ones, and the fallback becomes the exception.&lt;/p&gt;

&lt;p&gt;Set both lines on your own recorded outcomes, check them on data you did not use to set them, and never ask it anything that needs the whole document read before the answer exists. The title's question has a one-word answer; the work is in where you draw the two lines.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The public half is in the repo above, down to every per-item answer; the pipeline numbers are from our own production code and data.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://amankumar.ai/blogs/jev-measured" rel="noopener noreferrer"&gt;amankumar.ai&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>llm</category>
      <category>benchmark</category>
    </item>
    <item>
      <title>The median MCP server costs 3,150 tokens before your agent calls anything</title>
      <dc:creator>Aman</dc:creator>
      <pubDate>Sun, 09 Aug 2026 16:13:27 +0000</pubDate>
      <link>https://dev.to/onlyoneaman/the-median-mcp-server-costs-3150-tokens-before-your-agent-calls-anything-4np3</link>
      <guid>https://dev.to/onlyoneaman/the-median-mcp-server-costs-3150-tokens-before-your-agent-calls-anything-4np3</guid>
      <description>&lt;p&gt;Every MCP server you connect sends its full tool list to the model before the model does anything. You pay for that list on every single turn, whether the agent uses one of those tools or none of them.&lt;/p&gt;

&lt;p&gt;I had a rough sense this was expensive. I did not have a number. So I went and measured it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The method
&lt;/h2&gt;

&lt;p&gt;The MCP directory I maintain has about 10,500 servers with a remote HTTP endpoint. I took a random 60, sent each an unauthenticated &lt;code&gt;tools/list&lt;/code&gt;, and measured the JSON that came back.&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;subprocess&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&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;curl&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;-s&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;-m&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;10&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;-X&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;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-H&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-Type: application/json&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;-H&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;Accept: application/json, text/event-stream&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;-d&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;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jsonrpc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:1,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tools/list&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;

    &lt;span class="c1"&gt;# streamable-http servers may answer as SSE
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lstrip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data: &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="n"&gt;tools&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;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result&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;tools&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="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&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="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Token counts below are &lt;code&gt;chars / 4&lt;/code&gt;, the usual rough approximation. It is approximate on purpose: JSON with lots of short keys tokenises a bit worse than prose, so if anything these are optimistic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first finding was not the one I went looking for
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;27 of the 60 did not answer at all.&lt;/strong&gt; Timeouts, connection failures, and auth walls on a method that is supposed to be discoverable. Some of that is legitimate, plenty of servers gate &lt;code&gt;tools/list&lt;/code&gt; behind a key. But nearly half of a registry's remote entries not responding to a plain discovery call is its own kind of interesting, and it is worth knowing before you assume a directory listing means a working server.&lt;/p&gt;

&lt;p&gt;That left 33 servers with real numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a tool list actually costs
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;percentile&lt;/th&gt;
&lt;th&gt;tools&lt;/th&gt;
&lt;th&gt;tokens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;min&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;174&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p25&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;913&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;median&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;11&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3,150&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p75&lt;/td&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;td&gt;14,113&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;max&lt;/td&gt;
&lt;td&gt;84&lt;/td&gt;
&lt;td&gt;19,923&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Those two max figures are different servers, which turned out to be the most useful thing I learned.&lt;/p&gt;

&lt;p&gt;The distribution is the story. It is not a normal curve with a comfortable middle. Nine of the 33 cost under 1,000 tokens. &lt;strong&gt;Ten cost more than 10,000.&lt;/strong&gt; A third of what I sampled eats five figures of context before your agent has read a line of the actual task.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tool count is a bad proxy for cost
&lt;/h2&gt;

&lt;p&gt;I assumed the server with the most tools would be the most expensive. It is not close.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;84 tools → 7,569 tokens&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;54 tools → 19,923 tokens&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The 54-tool server costs two and a half times more with a third fewer tools. The difference is entirely schema verbosity: long descriptions, deeply nested parameter objects, enums spelled out inline, examples in the schema.&lt;/p&gt;

&lt;p&gt;So "I only connected a couple of small servers" is not the reassurance it sounds like. A tidy 10-tool server with chatty schemas can cost more than a 50-tool one with terse ones, and the tool count is the only number most clients show you.&lt;/p&gt;

&lt;p&gt;If you loaded all 33, you would spend roughly &lt;strong&gt;215,000 tokens&lt;/strong&gt; describing capabilities. That is most of a 200k window, gone, before anything happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that compounds
&lt;/h2&gt;

&lt;p&gt;Nobody connects 33 servers. But three or four is completely normal, and here is where it gets uncomfortable: you pay this on &lt;strong&gt;every turn&lt;/strong&gt;, not once per session. A twenty-turn conversation with four median servers is 3,150 × 4 × 20, about &lt;strong&gt;252,000 tokens&lt;/strong&gt; spent re-describing tools the model mostly will not call.&lt;/p&gt;

&lt;p&gt;At that point the tool definitions cost more than the conversation.&lt;/p&gt;

&lt;p&gt;There is a quality cost too, and I think it is the bigger one. More tools in context means more surface for the model to pick wrong. Anecdotally, the failure mode is not "cannot find the tool", it is "confidently picked the tool with the similar name". Twelve overlapping search tools do not make an agent twelve times better at searching.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways out
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Prune.&lt;/strong&gt; The cheapest fix and the one people skip. Most clients let you disable individual servers per project. A repo that never touches your calendar does not need the calendar server loaded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Filter at the client.&lt;/strong&gt; Some clients now support allow-lists per server, so you load 3 tools from a 40-tool server. Underrated, and it works today with no new infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Load on demand.&lt;/strong&gt; Do not send definitions up front at all. Give the model one small tool that searches for tools, and fetch the real definition only once it has chosen. You trade a round trip for the context.&lt;/p&gt;

&lt;p&gt;That last one is the approach I ended up building on, so treat the next section as an interested party talking.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we did
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://fetchbean.com" rel="noopener noreferrer"&gt;fetchbean&lt;/a&gt; is one endpoint over a catalog of tools, currently 56 providers and 748 of them. The agent does not get 748 definitions. It gets four meta-tools, and asks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"https://api.fetchbean.com/discover?q=read+a+page+my+fetch+got+403+on"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"results"&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="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"jina"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"endpoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Read a URL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"blurb"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Clean markdown from any URL."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"params"&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="s2"&gt;"url"&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="w"&gt;
&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="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then calls it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.fetchbean.com/v1/run &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-API-Key: &lt;/span&gt;&lt;span class="nv"&gt;$KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"provider":"jina","endpoint":"/read","input":{"url":"https://example.com"}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four definitions in context instead of 748. &lt;code&gt;discover&lt;/code&gt; needs no key, so you can try that first curl right now and see what comes back.&lt;/p&gt;

&lt;p&gt;The credential side turned out to matter as much as the context side. Every integration wants its own key, and those keys end up in the agent's config, which means your shell history, your repo, and eventually a log. Connected accounts are held encrypted server side and injected at call time, so the agent gets a result and never the credential.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it does not do:&lt;/strong&gt; if you need one API, call that API. A wrapper only pays off when you would otherwise juggle five signups, five keys and five error formats. You are also trusting a third party with credentials to services you care about, which is a real tradeoff and not one I want to gloss over. And it is per-call billing, cheap but not free past the first 5,000 calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveats on the numbers
&lt;/h2&gt;

&lt;p&gt;A random 60 from one directory is not the whole ecosystem, and the servers people actually use skew toward the well-maintained end. The &lt;code&gt;chars / 4&lt;/code&gt; approximation is rough. And the 27 that did not answer are excluded entirely, which probably biases the sample toward servers that are happy to talk to strangers, and those may well be simpler than the ones behind auth.&lt;/p&gt;

&lt;p&gt;The script is above. If you run it against the servers you actually have connected, I would genuinely like to see what you get, because the number that matters is yours, not my median.&lt;/p&gt;

&lt;h2&gt;
  
  
  So
&lt;/h2&gt;

&lt;p&gt;Go look at what you have connected right now and add up the tool counts. My guess is it is more than you think, and that a third of it is stuff your agent has never once called.&lt;/p&gt;

&lt;p&gt;What is in your context window that does not need to be?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building Agent Skills from Scratch</title>
      <dc:creator>Aman</dc:creator>
      <pubDate>Fri, 26 Dec 2025 08:20:23 +0000</pubDate>
      <link>https://dev.to/onlyoneaman/building-agent-skills-from-scratch-lbl</link>
      <guid>https://dev.to/onlyoneaman/building-agent-skills-from-scratch-lbl</guid>
      <description>&lt;p&gt;There's a lot written about agent skills, but not much about actually implementing them.&lt;/p&gt;

&lt;p&gt;This post shows you how they work and how to integrate them into your existing agent.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/onlyoneaman/agent-skills" rel="noopener noreferrer"&gt;View the complete implementation on GitHub&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Are Agent Skills?
&lt;/h2&gt;

&lt;p&gt;Agent skills solve a simple problem: your system prompt gets bloated when you try to make your agent good at everything.&lt;/p&gt;

&lt;p&gt;Instead of this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You're an expert at code review, git, file organization, API testing...
[2000 lines of instructions]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You have access to these skills:
- code-review: Reviews code for bugs and security
- git-helper: Git workflows and troubleshooting
- file-organizer: Organizes files intelligently
- api-tester: Tests REST APIs

Load them when needed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Core Idea
&lt;/h2&gt;

&lt;p&gt;Skills are markdown files that live in a directory. Each skill has two parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;YAML frontmatter with name and description (see &lt;a href="https://amankumar.ai/blogs/wtf-is-frontmatter" rel="noopener noreferrer"&gt;this guide on frontmatter&lt;/a&gt; if you're new to it)&lt;/li&gt;
&lt;li&gt;Markdown body with detailed instructions&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When the agent needs expertise, it loads the relevant skill on the fly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User: "Review this code for SQL injection"
  ↓
Agent: "I need the code-review skill"
  ↓
System: [Loads SKILL.md with security guidelines]
  ↓
Agent: [Follows those guidelines]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key insight is that skills are just structured prompts. But they're modular, discoverable, and loaded on demand.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Actually Works
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Discovery
&lt;/h3&gt;

&lt;p&gt;Scan a directory for SKILL.md files and parse their frontmatter. You only load the metadata initially, not the full content. This keeps memory usage low. (&lt;a href="https://github.com/onlyoneaman/agent-skills" rel="noopener noreferrer"&gt;See the discovery implementation&lt;/a&gt;)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;skills/
├── code-review/
│   └── SKILL.md        # name: code-review, description: ...
├── git-helper/
│   └── SKILL.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During discovery, you extract just the YAML frontmatter (name, description). The full markdown content stays on disk until needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Tool Registration
&lt;/h3&gt;

&lt;p&gt;Convert each skill into an &lt;a href="https://platform.openai.com/docs/guides/function-calling" rel="noopener noreferrer"&gt;OpenAI function tool&lt;/a&gt;. The LLM sees these as callable functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;activate_skill_code_review: "Reviews code for bugs, security, best practices"
activate_skill_git_helper: "Git workflows and troubleshooting"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The description is critical because it's what the LLM uses to decide which skill to activate. Be specific and clear.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Activation
&lt;/h3&gt;

&lt;p&gt;When the LLM calls a skill function:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Load the full &lt;code&gt;SKILL.md&lt;/code&gt; content from disk&lt;/li&gt;
&lt;li&gt;Add it to the conversation as a tool result&lt;/li&gt;
&lt;li&gt;Let the LLM continue with those instructions&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is lazy loading. You only fetch content when actually needed. If you have 20 skills but only use 2, you've only loaded 2.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Execution
&lt;/h3&gt;

&lt;p&gt;The LLM reads the skill instructions and follows them. The skill acts like a temporary system prompt for that specific task. Once the task is done, the skill instructions fade from context (unless you keep them for multi-turn conversations).&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Skill Looks Like
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;code-review&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Reviews code for bugs, security, and best practices&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1.0.0&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# Code Review Skill&lt;/span&gt;

You are an expert code reviewer.

&lt;span class="gu"&gt;## Check For&lt;/span&gt;
&lt;span class="p"&gt;
1.&lt;/span&gt; &lt;span class="gs"&gt;**Security**&lt;/span&gt;
&lt;span class="p"&gt;   -&lt;/span&gt; SQL injection in queries
&lt;span class="p"&gt;   -&lt;/span&gt; XSS in user inputs
&lt;span class="p"&gt;   -&lt;/span&gt; Auth bypasses
&lt;span class="p"&gt;
2.&lt;/span&gt; &lt;span class="gs"&gt;**Quality**&lt;/span&gt;
&lt;span class="p"&gt;   -&lt;/span&gt; Readability
&lt;span class="p"&gt;   -&lt;/span&gt; Maintainability
&lt;span class="p"&gt;   -&lt;/span&gt; DRY violations
&lt;span class="p"&gt;
3.&lt;/span&gt; &lt;span class="gs"&gt;**Performance**&lt;/span&gt;
&lt;span class="p"&gt;   -&lt;/span&gt; N+1 queries
&lt;span class="p"&gt;   -&lt;/span&gt; Memory leaks
&lt;span class="p"&gt;   -&lt;/span&gt; Inefficient algorithms

&lt;span class="gu"&gt;## Response Format&lt;/span&gt;

&lt;span class="gs"&gt;**Summary**&lt;/span&gt;: Brief assessment
&lt;span class="gs"&gt;**Critical Issues**&lt;/span&gt;: Security problems (if any)
&lt;span class="gs"&gt;**Improvements**&lt;/span&gt;: Suggestions for better code
&lt;span class="gs"&gt;**Positives**&lt;/span&gt;: What works well
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the structure: clear sections, bullet points, and expected output format. The LLM follows structured instructions much better than prose. (For more on crafting effective prompts, see &lt;a href="https://amankumar.ai/blogs/anatomy-of-a-prompt-the-complete-guide-to-crafting-effective-ai-chatgpt-prompts" rel="noopener noreferrer"&gt;this guide&lt;/a&gt;.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Pattern Works
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Context Efficiency&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of loading 10KB of instructions upfront, you load 100 bytes of metadata. Full instructions only come in when needed. This matters when you're paying per token.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Modularity&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Each skill is independent. Add a new one by dropping in a &lt;code&gt;SKILL.md&lt;/code&gt; file. No code changes needed. Want to remove a skill? Delete the directory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Clarity&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When debugging, you can see exactly which skill was activated and what instructions it provided. This makes troubleshooting much easier than a monolithic prompt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Reusability&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Share skills across projects. Someone else's &lt;code&gt;api-tester&lt;/code&gt; skill works in your agent with zero modification. Skills become a shared library of expertise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Design Decisions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Lazy Loading
&lt;/h3&gt;

&lt;p&gt;Don't load all skills into memory at startup. This defeats the purpose because you're back to loading everything upfront.&lt;/p&gt;

&lt;p&gt;Do load on demand. Parse frontmatter during discovery, but keep full content on disk until the LLM actually requests it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Function Naming
&lt;/h3&gt;

&lt;p&gt;Prefix skill functions clearly: &lt;code&gt;activate_skill_code_review&lt;/code&gt;. This makes it obvious in logs what's happening. When you see &lt;code&gt;activate_skill_*&lt;/code&gt; in your logs, you know a skill was activated.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conversation Flow
&lt;/h3&gt;

&lt;p&gt;The exact sequence matters. Here's what happens:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User sends message&lt;/li&gt;
&lt;li&gt;LLM responds with tool_calls (requesting a skill)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Critical&lt;/strong&gt;: Add assistant message with tool_calls to conversation&lt;/li&gt;
&lt;li&gt;Add tool message with skill content&lt;/li&gt;
&lt;li&gt;LLM continues with skill instructions&lt;/li&gt;
&lt;li&gt;Final response&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you skip step 3, OpenAI will reject your request. The tool_calls must be properly formatted with a type field and nested function object. This is a common gotcha. (See &lt;a href="https://platform.openai.com/docs/guides/function-calling" rel="noopener noreferrer"&gt;OpenAI's tools documentation&lt;/a&gt; for details.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Looping for Multiple Tool Calls
&lt;/h3&gt;

&lt;p&gt;Skills can chain. A skill might activate code execution, which might need another skill. Your agent should loop until there are no more tool calls:&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="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&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;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_calls&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="nf"&gt;handle_tool_calls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Always pass tools in every call, even after skill activation. Otherwise, skills can't use other tools like code execution. (&lt;a href="https://github.com/onlyoneaman/agent-skills" rel="noopener noreferrer"&gt;See full implementation&lt;/a&gt; for the complete loop logic.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Considerations
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Skill Scope&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One skill equals one domain. Keep them focused.&lt;/p&gt;

&lt;p&gt;Good examples: code-review, git-helper, api-tester&lt;br&gt;
Bad example: developer-tools (too broad)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skill Structure&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use clear sections with examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What the skill does&lt;/li&gt;
&lt;li&gt;How to approach tasks&lt;/li&gt;
&lt;li&gt;Expected output format&lt;/li&gt;
&lt;li&gt;Examples of good results&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A wall of text doesn't work. Structure helps the LLM follow instructions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error Handling&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What if a skill doesn't exist? Return a helpful error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Skill 'xyz' not found. Available: code-review, git-helper"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Common Mistakes &amp;amp; Troubleshooting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Loading Everything Upfront
&lt;/h3&gt;

&lt;p&gt;Some implementations load all skills at startup. This defeats the purpose because you're back to loading everything upfront, wasting memory and context tokens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix&lt;/strong&gt;: Load metadata only during discovery. Activate skills when needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Vague Skill Descriptions
&lt;/h3&gt;

&lt;p&gt;The LLM uses skill descriptions to decide which to activate. Be specific.&lt;/p&gt;

&lt;p&gt;❌ "Helps with code"&lt;br&gt;
✅ "Reviews Python/JavaScript code for security vulnerabilities, PEP 8 compliance, and performance issues"&lt;/p&gt;

&lt;p&gt;Include what the skill does, what types of tasks it handles, and key capabilities.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wrong Tool Calls Format
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Error&lt;/strong&gt;: Missing required parameter: messages[1].tool_calls[0].type&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause&lt;/strong&gt;: OpenAI requires a specific nested structure. The tool_calls must have a type field and nest the function details under a function key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix&lt;/strong&gt;: Use the correct format with type: "function" and nested function object. Don't flatten it. See &lt;a href="https://platform.openai.com/docs/guides/function-calling" rel="noopener noreferrer"&gt;OpenAI's tools documentation&lt;/a&gt; for the exact message format.&lt;/p&gt;

&lt;h3&gt;
  
  
  Forgetting to Include Tools After Skill Activation
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;: After activating a skill, the LLM can't use other tools like code execution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix&lt;/strong&gt;: Always pass tools in every LLM call. Don't remove tools after skill activation because skills might need them.&lt;/p&gt;

&lt;h3&gt;
  
  
  No Structure in Skills
&lt;/h3&gt;

&lt;p&gt;A wall of text doesn't work. Use clear headings, bullet points, code examples, and expected output formats. The LLM follows structured instructions much better than prose.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Skills Make Sense
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Good fit:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multi-domain agents that handle code, git, and devops&lt;/li&gt;
&lt;li&gt;Agents with specialized workflows&lt;/li&gt;
&lt;li&gt;Teams sharing common patterns&lt;/li&gt;
&lt;li&gt;When you hit context limits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Not needed:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Single-purpose agents&lt;/li&gt;
&lt;li&gt;Agents with small, focused prompts&lt;/li&gt;
&lt;li&gt;Prototypes and experiments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don't over-engineer. If your system prompt is small and manageable, you probably don't need skills.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Standard
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://agentskills.io" rel="noopener noreferrer"&gt;AgentSkills.io&lt;/a&gt; defines the open format:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SKILL.md naming convention&lt;/li&gt;
&lt;li&gt;YAML frontmatter schema&lt;/li&gt;
&lt;li&gt;Directory structure&lt;/li&gt;
&lt;li&gt;Best practices&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Following the standard means your skills work with other implementations. Skills become portable across projects and teams.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building Your First Skill
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Create the directory: &lt;code&gt;mkdir -p skills/my-first-skill&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create SKILL.md with YAML frontmatter and markdown instructions&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Integrate SkillsManager into your agent (&lt;a href="https://github.com/onlyoneaman/agent-skills" rel="noopener noreferrer"&gt;see GitHub repo for full code&lt;/a&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Test it by asking your agent to use the skill and verifying it activates&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it. No code changes needed to add new skills. Just drop in a SKILL.md file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;Agent skills are structured prompts with a loading mechanism.&lt;/p&gt;

&lt;p&gt;The pattern works because:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It keeps context lean by only loading what you need&lt;/li&gt;
&lt;li&gt;It makes agents modular since skills are independent&lt;/li&gt;
&lt;li&gt;It enables skill reuse so you can share skills across projects&lt;/li&gt;
&lt;li&gt;It simplifies debugging with clear activation logs&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can build a working implementation in an afternoon. The core SkillsManager is about 130 lines of Python. (&lt;a href="https://github.com/onlyoneaman/agent-skills" rel="noopener noreferrer"&gt;View the implementation&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Start with one skill. See if it helps. Expand from there.&lt;/p&gt;

&lt;p&gt;The complete working implementation is available on &lt;a href="https://github.com/onlyoneaman/agent-skills" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Use it as a reference or starting point for your own agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://agentskills.io" rel="noopener noreferrer"&gt;AgentSkills.io&lt;/a&gt; - Official specification&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/anthropics/claude-skills" rel="noopener noreferrer"&gt;Claude Skills&lt;/a&gt; - Anthropic's skill examples&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/samuelint/open-agent-skills" rel="noopener noreferrer"&gt;Open Agent Skills&lt;/a&gt; - Community skill library&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/onlyoneaman/agent-skills" rel="noopener noreferrer"&gt;Working Implementation&lt;/a&gt; - Complete code from this tutorial&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://amankumar.ai/blogs/wtf-is-frontmatter" rel="noopener noreferrer"&gt;WTF is Frontmatter?&lt;/a&gt; - Understanding frontmatter/metadata in markdown files&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://amankumar.ai/blogs/anatomy-of-a-prompt-the-complete-guide-to-crafting-effective-ai-chatgpt-prompts" rel="noopener noreferrer"&gt;Anatomy of a Prompt&lt;/a&gt; - Complete guide to crafting effective AI prompts with structured approaches&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://platform.openai.com/docs/guides/function-calling" rel="noopener noreferrer"&gt;OpenAI Function Calling&lt;/a&gt; - Official OpenAI tools documentation&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
      <category>skills</category>
    </item>
    <item>
      <title>I cut my site’s image payload by 97.7% (83.57 MB 1.89 MB)</title>
      <dc:creator>Aman</dc:creator>
      <pubDate>Tue, 23 Dec 2025 06:36:46 +0000</pubDate>
      <link>https://dev.to/onlyoneaman/i-cut-my-sites-image-payload-by-977-8357-mb-189-mb-3338</link>
      <guid>https://dev.to/onlyoneaman/i-cut-my-sites-image-payload-by-977-8357-mb-189-mb-3338</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%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A1400%2Fformat%3Awebp%2F0%2Aa9R-cspPepIXAYRJ" 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%2Fmiro.medium.com%2Fv2%2Fresize%3Afit%3A1400%2Fformat%3Awebp%2F0%2Aa9R-cspPepIXAYRJ" alt="captionless image" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I run &lt;a href="https://amankumar.ai/" rel="noopener noreferrer"&gt;amankumar.ai&lt;/a&gt; on Next.js. Pages would load, layouts would appear, and React would do its thing, but something still felt slow.&lt;/p&gt;

&lt;p&gt;The problem wasn’t that the page didn’t render.&lt;/p&gt;

&lt;p&gt;The problem was this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The page renders, but images stay blank for a moment. Then they drop in one by one. And the site doesn’t feel visually complete until later.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That gap, where placeholders sit there and images arrive late, is subtle, but once you notice it, you can’t unsee it. It makes the site feel heavier than it should.&lt;/p&gt;

&lt;p&gt;So I stopped guessing and looked at the boring part I’d been ignoring: &lt;strong&gt;image payloads&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this keeps happening on modern sites (without anyone messing up)
&lt;/h2&gt;

&lt;p&gt;This problem shows up a lot more today, even on well-built sites, and it’s not because people are careless.&lt;/p&gt;

&lt;p&gt;A few things are happening at once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Design tools export big images by default&lt;/strong&gt; Posters, UI mockups, and screenshots often come out as large PNGs.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;AI-generated images are heavy by nature&lt;/strong&gt; High detail, high resolution, no concern for delivery size.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Frameworks don’t change asset intent&lt;/strong&gt; Next.js can optimize &lt;em&gt;delivery&lt;/em&gt;, but if the source image is huge, it still has to download.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Image bloat accumulates quietly&lt;/strong&gt; No errors, no warnings, just slower visual completion over time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is exactly what I was seeing: the page loads, but images arrive last.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measuring the problem on amankumar.ai
&lt;/h2&gt;

&lt;p&gt;Before touching anything, I measured the image assets in the repo.&lt;/p&gt;

&lt;p&gt;They were a mix of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  PNGs (most of them)&lt;/li&gt;
&lt;li&gt;  some JPG/JPEGs&lt;/li&gt;
&lt;li&gt;  different resolutions&lt;/li&gt;
&lt;li&gt;  photos, posters, and UI graphics, all mixed together&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After running a single optimization pass, the numbers looked like this:&lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2Fg68kirvfcjy1jgk58qak.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%2Fg68kirvfcjy1jgk58qak.png" alt="Optimization Results for amankumar.ai" width="800" height="315"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is &lt;strong&gt;repo-level image weight&lt;/strong&gt;, not “every page ships 83 MB.” But it clearly showed the root issue. I was carrying a lot of unnecessary image data, and the browser was paying for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The engineering question I cared about
&lt;/h2&gt;

&lt;p&gt;I didn’t want a clever setup. I wanted something:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  repeatable&lt;/li&gt;
&lt;li&gt;  boring&lt;/li&gt;
&lt;li&gt;  safe for mixed assets&lt;/li&gt;
&lt;li&gt;  easy to re-run later&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the real question became:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What’s the simplest image pipeline that’s correct most of the time?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  First principle: pixels matter more than formats
&lt;/h2&gt;

&lt;p&gt;Before debating PNG vs WebP vs AVIF, the biggest issue was &lt;strong&gt;pixel count&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If an image is 2400px wide but is never rendered above ~800px, shipping the extra pixels is pure waste.&lt;/p&gt;

&lt;p&gt;So I made one hard rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Cap the max dimension at 800px.&lt;/strong&gt; If an image is larger, downscale it. If it’s smaller, leave it alone.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This single decision removes a surprising amount of weight.&lt;/p&gt;

&lt;h2&gt;
  
  
  Image formats: quality vs size (same perceived quality)
&lt;/h2&gt;

&lt;p&gt;Once dimensions are sane, format choice actually matters.&lt;/p&gt;

&lt;p&gt;Here’s the practical ranking when comparing images at roughly the same visual quality:&lt;/p&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.amazonaws.com%2Fuploads%2Farticles%2Fsgm47m2amdjoxkf94l0e.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%2Fsgm47m2amdjoxkf94l0e.png" alt="Comparing Image Formats" width="800" height="308"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;AVIF genuinely compresses better than WebP in many cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I didn’t standardize on AVIF (yet)
&lt;/h2&gt;

&lt;p&gt;This is important, because AVIF &lt;em&gt;is&lt;/em&gt; genuinely impressive.&lt;/p&gt;

&lt;p&gt;The reason I didn’t standardize on it is &lt;strong&gt;not quality&lt;/strong&gt;. It’s &lt;strong&gt;practical browser support and operational safety&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;According to current browser compatibility data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  AVIF support is &lt;strong&gt;good&lt;/strong&gt;, but not universal&lt;/li&gt;
&lt;li&gt;  Some older browsers, embedded webviews, and edge cases still fall back poorly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can see the current state clearly here: &lt;a href="https://caniuse.com/?search=avif" rel="noopener noreferrer"&gt;https://caniuse.com/?search=avif&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For this project, I didn’t want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  extra format negotiation&lt;/li&gt;
&lt;li&gt;  more fallbacks&lt;/li&gt;
&lt;li&gt;  “why didn’t this image load on X device?” debugging&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the decision wasn’t “AVIF is bad.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Get Aman Kumar’s stories in&amp;nbsp;your&amp;nbsp;inbox
&lt;/h2&gt;

&lt;p&gt;Join Medium for free to get updates from&amp;nbsp;this&amp;nbsp;writer.&lt;/p&gt;

&lt;p&gt;Subscribe&lt;/p&gt;

&lt;p&gt;Subscribe&lt;/p&gt;

&lt;p&gt;It was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;WebP is the safest modern default today.&lt;/strong&gt; AVIF can be layered later if needed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The tricky part: photos vs UI/posters
&lt;/h2&gt;

&lt;p&gt;My repo wasn’t just photos.&lt;/p&gt;

&lt;p&gt;It had:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  UI graphics&lt;/li&gt;
&lt;li&gt;  posters with text&lt;/li&gt;
&lt;li&gt;  logos and illustrations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you treat everything like a photo and apply lossy compression everywhere, UI assets suffer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  fuzzy text&lt;/li&gt;
&lt;li&gt;  halos around edges&lt;/li&gt;
&lt;li&gt;  cheap-looking posters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;WebP helped here because it supports &lt;strong&gt;both lossy and lossless modes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The challenge was choosing between them &lt;em&gt;without manual tagging&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The simple rule that worked
&lt;/h2&gt;

&lt;p&gt;I used one clean signal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;If the image has transparency (alpha)&lt;/strong&gt;, likely UI or graphic, use &lt;strong&gt;lossless WebP&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;If it’s fully opaque&lt;/strong&gt;, likely a photo, use &lt;strong&gt;lossy WebP&lt;/strong&gt; (quality ~80)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Is this perfect? No. Is it correct often enough to automate safely? Yes.&lt;/p&gt;

&lt;p&gt;That single rule handled mixed assets without human intervention.&lt;/p&gt;

&lt;h2&gt;
  
  
  Metadata: invisible weight
&lt;/h2&gt;

&lt;p&gt;Many images carry metadata:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  camera EXIF&lt;/li&gt;
&lt;li&gt;  editing history&lt;/li&gt;
&lt;li&gt;  embedded profiles&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this helps a webpage load faster or look better.&lt;/p&gt;

&lt;p&gt;So I strip metadata unconditionally. Sometimes the savings are small; sometimes they’re large. Either way, it’s free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sanity check on a very different repo
&lt;/h2&gt;

&lt;p&gt;To make sure this wasn’t a one-off, I ran the &lt;strong&gt;same script, unchanged&lt;/strong&gt;, on another repo: &lt;a href="https://promptsmint.com/" rel="noopener noreferrer"&gt;promptsmint.com&lt;/a&gt;, an AI prompts library with only AI-generated images.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before optimization&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  92 images&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;228.12 MB total&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;2.48 MB per image (avg)&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;After optimization&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  92 images&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;4.28 MB total&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;~50 KB per image (avg)&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;98.0% reduction&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;223.84 MB saved&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Different site. Different content. Same outcome.&lt;/p&gt;

&lt;p&gt;That confirmed this wasn’t luck. It was just removing waste.&lt;/p&gt;

&lt;h2&gt;
  
  
  The script
&lt;/h2&gt;

&lt;p&gt;I packaged the whole pipeline into a single script and hosted it here:&lt;/p&gt;

&lt;p&gt;&lt;b&gt;[other]script[/other]&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;It:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  finds all &lt;em&gt;jpg/jpeg/png/webp&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;  outputs &lt;em&gt;*-optimized.webp&lt;/em&gt; next to originals&lt;/li&gt;
&lt;li&gt;  caps size at &lt;strong&gt;800px&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;  strips metadata&lt;/li&gt;
&lt;li&gt;  uses &lt;strong&gt;lossless WebP for transparent images&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;  uses &lt;strong&gt;lossy WebP for opaque images&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No clever tricks. Just consistent rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual outcome
&lt;/h2&gt;

&lt;p&gt;I didn’t magically make Next.js faster.&lt;/p&gt;

&lt;p&gt;What changed is simpler and more important:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Images arrive earlier. Placeholders disappear sooner. Pages feel visually complete faster.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That was the slowness I was noticing, and this fixed it.&lt;/p&gt;

&lt;p&gt;Thanks for reading. If you found this useful, you can follow or connect with me here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://x.com/onlyoneaman/" rel="noopener noreferrer"&gt;x.com&lt;/a&gt; &lt;a href="https://www.linkedin.com/in/onlyoneaman/" rel="noopener noreferrer"&gt;linkedin&lt;/a&gt;&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>web</category>
      <category>webp</category>
      <category>images</category>
    </item>
    <item>
      <title>I Tested 7 Python PDF Extractors So You Don’t Have To (2025 Edition)</title>
      <dc:creator>Aman</dc:creator>
      <pubDate>Thu, 18 Dec 2025 13:14:00 +0000</pubDate>
      <link>https://dev.to/onlyoneaman/i-tested-7-python-pdf-extractors-so-you-dont-have-to-2025-edition-akm</link>
      <guid>https://dev.to/onlyoneaman/i-tested-7-python-pdf-extractors-so-you-dont-have-to-2025-edition-akm</guid>
      <description>&lt;h3&gt;
  
  
  Why This Even Matters
&lt;/h3&gt;

&lt;p&gt;PDF extraction sounds boring until you need it. Then it becomes the bottleneck in everything you’re trying to build.&lt;/p&gt;

&lt;p&gt;Maybe you’re building a document search system and need clean text for indexing. Maybe you’re creating embeddings for a RAG pipeline, and garbage text means garbage vectors. Maybe you’re processing invoices, analysing research papers, or just trying to get data out of that quarterly report someone sent you.&lt;/p&gt;

&lt;p&gt;For small PDFs? Sure, you can often just pass the whole thing to Claude or GPT-4. But when you’re dealing with hundreds of documents, building search systems, or need structured data for processing, that’s when extraction quality actually matters.&lt;/p&gt;

&lt;p&gt;So I decided to test the most popular Python libraries the way most developers would actually use them: minimal setup, basic extraction, real-world document.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Actually Tested
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Document&lt;/strong&gt;: A typical business PDF — one page with headers, body text, a six-column table, and an image. The kind of thing that shows up in email attachments daily. You can find it &lt;a href="https://bella.amankumar.ai/examples/pdf/11-google-doc-document/google-doc-document.pdf" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Environment&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  MacBook M2 Pro, 16GB RAM, macOS 15&lt;/li&gt;
&lt;li&gt;  Fresh Python 3.11 virtual environment&lt;/li&gt;
&lt;li&gt;  Clean pip installs, no optimisations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can find the code for this test &lt;a href="https://gist.github.com/onlyoneaman/a479b3875524d39cee234f013866015e" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Testing Approach&lt;/strong&gt;: I used the simplest possible implementation for each library — the approach you’d try first when you’re just getting started. Most of these packages have advanced configuration options, specialised table extraction methods, and layout analysis features that could dramatically change results. But I wanted to see what you get with minimal effort.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I Measured&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Speed&lt;/strong&gt;: How fast can it process a single page?&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Text Quality&lt;/strong&gt;: Is the output readable and properly formatted?&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Table Handling&lt;/strong&gt;: Do tables survive extraction in usable form?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Libraries (How to test quickly &amp;amp; Honest First Impressions)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  pypdfium2 — The Speed Champion
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# pip install pypdfium2
import pypdfium2 as pdfium
text = "\n".join(
    p.get_textpage().get_text_range() 
    for p in pdfium.PdfDocument("doc.pdf")
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What I got&lt;/strong&gt;: Clean, readable text in 0.004 seconds. No formatting, no table structure — just fast, basic extraction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good for&lt;/strong&gt;: High-volume processing, simple content indexing, when speed matters more than structure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consider if&lt;/strong&gt; you need any formatting preservation or structured data extraction.&lt;/p&gt;

&lt;h3&gt;
  
  
  pypdf — The Reliable Default
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# pip install pypdf
from pypdf import PdfReader
reader = PdfReader("doc.pdf")
text = "\n".join(p.extract_text() for p in reader.pages)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What I got&lt;/strong&gt;: Solid text extraction with occasional spacing quirks. Works everywhere, no C dependencies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good for&lt;/strong&gt;: Lambda functions, containerised apps, environments where you can’t compile extensions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consider if&lt;/strong&gt;: Text fidelity is critical for your downstream processing.&lt;/p&gt;

&lt;h3&gt;
  
  
  pdfplumber — The Data Extraction Tool
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# pip install pdfplumber
import pdfplumber
with pdfplumber.open("doc.pdf") as pdf:
    first_page = pdf.pages[0]
    text = first_page.extract_text()
    table = first_page.extract_table()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What I got&lt;/strong&gt;: Basic text had some concatenation issues, but the table extraction worked well. This library has extensive options for fine-tuning that I didn’t explore.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good for&lt;/strong&gt;: When you specifically need tabular data, coordinate-based extraction, or detailed layout control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consider if&lt;/strong&gt;: You just need clean text without heavy configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  pymupdf4llm — The Markdown Generator
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# pip install pymupdf4llm
import pymupdf4llm
markdown = pymupdf4llm.to_markdown("doc.pdf")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What I got&lt;/strong&gt;: Clean markdown output in 0.14 seconds with proper headings and table formatting. Surprisingly good results.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good for&lt;/strong&gt;: Content systems, documentation processing, when you need structured text that preserves hierarchy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consider if&lt;/strong&gt;: You’re dealing with complex multi-column layouts that might get scrambled.&lt;/p&gt;

&lt;h3&gt;
  
  
  unstructured — The Semantic Chunker
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# pip install "unstructured[all-docs]"
from unstructured.partition.auto import partition
blocks = partition(filename="doc.pdf")
for block in blocks:
    print(f"{block.category}: {block.text}")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What I got&lt;/strong&gt;: Semantically labelled chunks (Title, NarrativeText, etc.) in 1.11 seconds. Perfect for downstream processing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good for&lt;/strong&gt;: RAG systems, document analysis, when you need meaningful content boundaries for embeddings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consider if&lt;/strong&gt;: You just need raw text content without semantic analysis.&lt;/p&gt;

&lt;h3&gt;
  
  
  marker-pdf — The Layout Perfectionist
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# pip install marker-pdf
from marker.converters.pdf import PdfConverter
from marker.models import create_model_dict
from marker.output import text_from_rendered
text, _, _ = text_from_rendered(
    PdfConverter(create_model_dict())("doc.pdf")
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What I got&lt;/strong&gt;: Stunning layout-perfect markdown with inline images. Takes 12 seconds and downloads a 1GB model on first run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good for&lt;/strong&gt;: When layout fidelity is critical, vision model inputs, and high-quality document conversion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consider if&lt;/strong&gt;: You’re processing documents in real-time or have resource constraints.&lt;/p&gt;

&lt;h3&gt;
  
  
  textract — The Universal Handler
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# pip install textract  # Requires Tesseract
import textract
text = textract.process("doc.pdf").decode()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What I got&lt;/strong&gt;: Fast extraction (0.05s) with automatic OCR fallback capability. Handles many file formats beyond PDF.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good for&lt;/strong&gt;: Mixed document types, when some files might be scanned, and building robust document processing pipelines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consider if&lt;/strong&gt;: You only handle digital PDFs and want to avoid additional dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Performance Results
&lt;/h2&gt;

&lt;p&gt;Here’s what actually happened with my test document:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;marker-pdf&lt;/strong&gt; (11.3s): Perfect structure preservation, ideal for high-quality conversions, long time though&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;pymupdf4llm&lt;/strong&gt; (0.12s): Excellent markdown output, great balance of speed and quality&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;unstructured&lt;/strong&gt; (1.29s): Clean semantic chunks, perfect for RAG workflows&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;textract&lt;/strong&gt; (0.21s): Fast with OCR capabilities, minor formatting variations&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;pypdfium2&lt;/strong&gt; (0.003s): Blazing speed, clean basic text, no structure&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;pypdf&lt;/strong&gt; (0.024s): Reliable extraction, occasional spacing artifacts&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;pdfplumber&lt;/strong&gt; (0.10s): Good for tables, text extraction needs configuration&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important caveat&lt;/strong&gt;: These results reflect basic usage with minimal configuration. Each library has advanced features that could significantly change performance for specific use cases. You can find the link to all results in the references.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Context matters more than raw performance&lt;/strong&gt;. The “best” extractor depends entirely on what you’re building and how you’ll use the extracted text.&lt;br&gt;
&lt;strong&gt;Simple often wins&lt;/strong&gt;. For many use cases, basic text extraction is perfectly adequate. Don’t over-engineer unless you actually need the advanced features.&lt;br&gt;
&lt;strong&gt;Test with your data&lt;/strong&gt;. PDF structures vary wildly. What works great on my test document might fail on your quarterly reports.&lt;br&gt;
&lt;strong&gt;Have a fallback plan&lt;/strong&gt;. For production systems, consider hybrid approaches: fast extraction first, and more sophisticated methods for edge cases.&lt;br&gt;
&lt;strong&gt;Advanced features exist&lt;/strong&gt;. This comparison only scratched the surface. Most libraries have configuration options that could completely change the results.&lt;/p&gt;

&lt;h3&gt;
  
  
  Next Steps
&lt;/h3&gt;

&lt;p&gt;The scope of this experiment was limited, but it could be useful for most of the basic use cases around extraction. Next, I will try to solve more problems like the ones listed below. If you have anything else we should add, please let me know.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;More Document types (DOC, DOCX, …)&lt;/strong&gt; are also very common document formats we come across, we either need to convert them to some common format or check the package for compatibility with it&lt;/li&gt;
&lt;li&gt;  Handling &lt;strong&gt;Password-Protected PDFs&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Dealing with OCR Text&lt;/strong&gt;: PDF files may contain scanned images of text, which cannot be extracted using standard methods. To handle OCR (Optical Character Recognition) text, specialised libraries like pytesseract (a wrapper for Google’s Tesseract OCR engine) can be used to extract text from the images.&lt;/li&gt;
&lt;li&gt;  Even in PDFs / Documents, there are more edge cases like handling images inside documents, OCR, vector drawings, and more.&lt;/li&gt;
&lt;li&gt;  Forms, especially with checkboxes&lt;/li&gt;
&lt;li&gt;  Rotated PDFs&lt;/li&gt;
&lt;li&gt;  Right-to-left script (Arabic/Hebrew).&lt;/li&gt;
&lt;li&gt;  DOCX containing elements like an embedded Excel chart or a floating text box.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;Pick the tool that fits your actual requirements, not the one with the highest benchmark scores.&lt;/p&gt;

&lt;p&gt;For most document processing needs, &lt;strong&gt;pymupdf4llm&lt;/strong&gt; hits the sweet spot of speed and quality. For RAG systems, &lt;strong&gt;unstructured&lt;/strong&gt; gives you better semantic chunks. For pure speed, &lt;strong&gt;pypdfium2&lt;/strong&gt; is hard to beat.&lt;/p&gt;

&lt;p&gt;But honestly? The extraction is usually the easy part. The real work happens in how you process, chunk, and use that text afterwards.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Found different results with your documents or discovered better approaches? I’d love to hear about it — this space moves fast, and real-world feedback keeps comparisons honest.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I hope you have found this article useful 😄. Thank you for reading. I am&lt;/p&gt;

&lt;p&gt;Follow me on &lt;a href="https://medium.com/" rel="noopener noreferrer"&gt;Medium&lt;/a&gt; for more articles. Also, let’s connect &lt;a href="https://twitter.com/onlyoneaman" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/onlyoneaman/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;. ☕️&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;References:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bella.amankumar.ai/examples/pdf/11-google-doc-document/google-doc-document.pdf" rel="noopener noreferrer"&gt;https://bella.amankumar.ai/examples/pdf/11-google-doc-document/google-doc-document.pdf&lt;/a&gt; (Test Python File)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gist.github.com/onlyoneaman/a479b3875524d39cee234f013866015e" rel="noopener noreferrer"&gt;https://gist.github.com/onlyoneaman/a479b3875524d39cee234f013866015e&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Final Results ZIP: &lt;a href="http://bella.amankumar.ai/examples/pdf/1.zip" rel="noopener noreferrer"&gt;http://bella.amankumar.ai/examples/pdf/1.zip&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/py-pdf/benchmarks?source=post_page-----c88013922257---------------------------------------" rel="noopener noreferrer"&gt;GitHub - py-pdf/benchmarks&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pymupdf.readthedocs.io/en/latest/pymupdf4llm/?source=post_page-----c88013922257---------------------------------------" rel="noopener noreferrer"&gt;pymupdf.readthedocs.io&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pypi.org/project/marker-pdf/?source=post_page-----c88013922257---------------------------------------" rel="noopener noreferrer"&gt;marker-pdf&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pypi.org/project/aiwand/?source=post_page-----c88013922257---------------------------------------" rel="noopener noreferrer"&gt;aiwand - pypi.org&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://amankumar.ai/?source=post_page-----c88013922257---------------------------------------" rel="noopener noreferrer"&gt;amankumar.ai&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>python</category>
      <category>extraction</category>
    </item>
    <item>
      <title>Export Your Brain: A Simple Way To Make Any AI “Know You” From Day One</title>
      <dc:creator>Aman</dc:creator>
      <pubDate>Wed, 17 Dec 2025 06:24:56 +0000</pubDate>
      <link>https://dev.to/onlyoneaman/export-your-brain-a-simple-way-to-make-any-ai-know-you-from-day-one-2bnd</link>
      <guid>https://dev.to/onlyoneaman/export-your-brain-a-simple-way-to-make-any-ai-know-you-from-day-one-2bnd</guid>
      <description>&lt;p&gt;Every new AI tool treats you like a stranger.&lt;/p&gt;

&lt;p&gt;New chat. New model. New product.&lt;br&gt;
Same ritual:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“I’m a dev, I use React/TS/Tailwind.”&lt;/li&gt;
&lt;li&gt;“Keep it short, show code first.”&lt;/li&gt;
&lt;li&gt;“Don’t give me motivational quotes.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You’ve already trained one AI on all of this.&lt;br&gt;
It knows your stack, your tone, your quirks, your life context.&lt;/p&gt;

&lt;p&gt;The question is: &lt;strong&gt;how do you steal that “you” and carry it anywhere?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That’s what this article is about:&lt;br&gt;
Not “exporting ChatGPT data”, but &lt;strong&gt;exporting your persona&lt;/strong&gt; so any AI, any agent, any system can plug into it.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Real Problem: Your Context Is Trapped
&lt;/h2&gt;

&lt;p&gt;We’re in a multi-AI world:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ChatGPT for ideation / debugging&lt;/li&gt;
&lt;li&gt;Claude / Gemini for writing&lt;/li&gt;
&lt;li&gt;Cursor / Copilot / Replit for coding&lt;/li&gt;
&lt;li&gt;Internal bots at work&lt;/li&gt;
&lt;li&gt;Random tools for notes, docs, email, scheduling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each one gets a tiny slice of you.&lt;br&gt;
None of them truly “know you”.&lt;/p&gt;

&lt;p&gt;Your preferences, your working style, your constraints, your goals – they’re all split across chats, tabs, and products.&lt;/p&gt;

&lt;p&gt;What we actually want:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“My AI context should be as portable as my email address.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Until the tools natively support that, we hack it:&lt;br&gt;
we create a &lt;strong&gt;Persona Memo&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Idea: A Persona Memo You Can Plug Anywhere
&lt;/h2&gt;

&lt;p&gt;Instead of trying to export raw chat history, you ask your main AI:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Look at everything you know about me.&lt;br&gt;
Compress it into a memo + JSON that any other system can use to work with me.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This does three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Gives &lt;em&gt;you&lt;/em&gt; a clean “Operating Manual” of yourself.&lt;/li&gt;
&lt;li&gt;Gives &lt;em&gt;other AIs&lt;/em&gt; a high-signal context to start from.&lt;/li&gt;
&lt;li&gt;Works &lt;strong&gt;across tools&lt;/strong&gt;: ChatGPT, Claude, Grok, Gemini, Cursor, internal agents, whatever.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You run one good prompt → get back a human-readable memo + machine-readable persona.&lt;/p&gt;

&lt;p&gt;That’s your portable brain.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Master Prompt (Use This On Whatever Knows You Best)
&lt;/h2&gt;

&lt;p&gt;Take this and paste it into the AI where you’ve spent the most time (ChatGPT, Claude, etc.):&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;




&lt;p&gt;What you get back:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Operating Manual&lt;/strong&gt; – human-readable, like a one-pager about “How to work with ”.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persona JSON&lt;/strong&gt; – machine-readable, ready to be plugged into agents, system prompts, configs, etc.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is already a massive upgrade over “Hi, I like React and short answers”.&lt;/p&gt;




&lt;h2&gt;
  
  
  How To Use This Persona In The Wild
&lt;/h2&gt;

&lt;p&gt;Once you’ve generated it, you now have a &lt;strong&gt;portable context pack&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You can:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Start any new AI chat like this&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;“Here’s my persona. Assume all of this as background whenever you answer.”&lt;br&gt;
&lt;em&gt;[paste Operating Manual or a trimmed version]&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Set it as “custom instructions” / “system prompt”&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Many tools let you define persistent instructions.&lt;br&gt;
   Drop your memo there and stop repeating yourself.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Wire it into your own agents&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Store the JSON in your DB or config&lt;/li&gt;
&lt;li&gt;Load it into your backend when instantiating an LLM&lt;/li&gt;
&lt;li&gt;Prepend it in the system or developer prompt&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your internal bots now behave like they’ve worked with you for months.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Use it as onboarding material&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can even share a cleaned-up version with collaborators:&lt;br&gt;
   “Here’s how I work, this will save both of us time.”&lt;/p&gt;


&lt;h2&gt;
  
  
  Keep It Tight: Version, Don’t Worship It
&lt;/h2&gt;

&lt;p&gt;A few practical notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Update it every 1–3 months&lt;/strong&gt;&lt;br&gt;
Your stack, goals, priorities shift. Rerun the prompt or manually edit.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Treat it like config, not scripture&lt;/strong&gt;&lt;br&gt;
If something feels wrong, change it. This is &lt;em&gt;your&lt;/em&gt; OS.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Be selective with where you paste it&lt;/strong&gt;&lt;br&gt;
Don’t feed deeply personal / sensitive stuff to random SaaS forms.&lt;br&gt;
Keep a “public-safe” persona and a more detailed internal one if needed.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Quick “Lite” Prompt (When You Don’t Need JSON)
&lt;/h2&gt;

&lt;p&gt;Sometimes you just want a small memo you can paste into a one-off tool.&lt;/p&gt;

&lt;p&gt;Here’s a shorter version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are an expert integration developer and personal-knowledge summariser.

Based on everything you know about me in this workspace, create a concise "Operating Manual" for working with me that I can paste into ANY AI tool.

Include sections:
- Who I am (1–2 lines)
- How I work best
- How to talk to me
- My tech / domain preferences
- What to double-check with me
- Things to avoid
- What I’m currently optimising for

Maximum 350 words. Use direct, confident language. Prioritise stable patterns over one-off details.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is your “carry in pocket” version.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Point Of All This
&lt;/h2&gt;

&lt;p&gt;You’re already training these models every day just by talking to them.&lt;/p&gt;

&lt;p&gt;Might as well &lt;strong&gt;capture&lt;/strong&gt; that and reuse it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One good prompt&lt;/li&gt;
&lt;li&gt;One persona memo&lt;/li&gt;
&lt;li&gt;Works across models, products, and your own agents&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So next time you open a new AI tool, you’re not introducing yourself.&lt;/p&gt;

&lt;p&gt;You’re just handing over your Operating Manual and saying:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Start here. We’ve got work to do.”&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>prompt</category>
      <category>promptengineering</category>
    </item>
    <item>
      <title>How to Make Rails Response Faster</title>
      <dc:creator>Aman</dc:creator>
      <pubDate>Thu, 30 Mar 2023 20:16:17 +0000</pubDate>
      <link>https://dev.to/onlyoneaman/how-to-make-rails-response-faster-19ff</link>
      <guid>https://dev.to/onlyoneaman/how-to-make-rails-response-faster-19ff</guid>
      <description>&lt;p&gt;As your document or response size increases 📈, it can result in much slower response time 📈 📈 , even in seconds 😿 which results in a pretty bad user experience. Speed is the key here 🚤, the quicker your site is the more your users like it. PageSpeed and Responses Size are also responsible for SEO Rankings and is a major part of &lt;a href="https://developers.google.com/speed" rel="noopener noreferrer"&gt;LightSpeed&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the below section, we explore how this is achieved and tested, if you have a code to fix 🐛, Just Jump to Implementation here and check this out later.&lt;/p&gt;

&lt;h3&gt;
  
  
  So, How do we achieve this?
&lt;/h3&gt;

&lt;p&gt;I found Rack::Deflater recently, and regret how I wasn’t using it for a long time. I experimented over Rack::Deflater by sending a heavy json as response and comparing the performance of rails app with and without Rack::Deflater . Below we can see the Package Size of 8.3 Mb which was transferred without Rack::Deflater and 359 Kb which was transferred with Rack::Deflater. The Actual Factor and the ratio of Compression can vary a bit, it consistently results in a faster response, thus undoubtedly Rack::Deflater does makes the user experience better by compressing and sending resources.&lt;/p&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%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F1%2AKbgjoCoVJFU89m8W876fEw.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%2Fcdn-images-1.medium.com%2Fmax%2F2000%2F1%2AKbgjoCoVJFU89m8W876fEw.png" alt="Size of package transferred over network with and without Rack::Deflater" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So how does Rack::Deflater does this. Rackk Deflate compresses the body of web page before responding to a request. When the response is then received by the client, it sees the gzip compression enabled via the header Content-Encoding=gzip and unzips it before rendering it. Though it sounds like a lot of trouble, remember generally CPUs are fast, and networks are slow. Thus, It’s much faster and more efficient to just send less data “over the internet” even if we spend time compressing and expanding that data.&lt;/p&gt;

&lt;p&gt;“Transferred” is the compressed size of all resources. You can think of it as the amount of upload and download data that a mobile user will use in order to load this page. “Resources” is the uncompressed size of all resources.&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementation
&lt;/h3&gt;

&lt;p&gt;Just add the below line in your application.rb file.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*config.middleware.use Rack::Deflater*
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Your file should be like this after it.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;....
class Application &amp;lt; Rails::Application
  ...
  *config.middleware.use Rack::Deflater
  ...
end
....*
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  TroubleShooting
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Check if you have placed the middleware line correctly in code.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If you are using ActionDispatch::Static in your app. Then,Rack::Deflater should be placed after ActionDispatch::Static. The reasoning is that if your app is also serving static assets (e.g., Heroku), when assets are served from disk they are already compressed. Inserting it before would only end up in Rack::Deflater attempting to re-compress those assets. Therefore as a performance optimisation:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    # application.rb

    config.middleware.insert_after ActionDispatch::Static, Rack::Deflater 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;If you are using any other middleware it might conflict withRack::Deflater , it should work if you use insert_before (instead of "use"), to place it near the top of the middleware stack, prior to any other middleware that might send a response. .use places it at the bottom of the stack. Let’s say, the topmost middleware is Rack::Sendfile. So we would use:&lt;/p&gt;

&lt;p&gt;config.middleware.insert_before(Rack::Sendfile, Rack::Deflater) &lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can get the list of middleware in order of loading by doing rake middleware from the command line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;I hope you have found this useful 😄. Thank you for reading.&lt;br&gt;
Drop a Response or 📬 if you have any queries or just wanna connect ☕️.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
    </item>
    <item>
      <title>React Hooks and Lifecycle Methods</title>
      <dc:creator>Aman</dc:creator>
      <pubDate>Wed, 29 Mar 2023 09:19:16 +0000</pubDate>
      <link>https://dev.to/onlyoneaman/react-hooks-and-lifecycle-methods-1kdo</link>
      <guid>https://dev.to/onlyoneaman/react-hooks-and-lifecycle-methods-1kdo</guid>
      <description>&lt;p&gt;React State and Lifecycle are very useful methods and with the advancement of React hooks and when a developer uses hooks instead of traditional React classes the most important question becomes how one is gonna implement the lifecycle methods offered by React classes in Hooks. We will look after the Hooks implementation of various lifecycle methods in this blog.&lt;/p&gt;

&lt;p&gt;If you are new to state and lifecycle, Have a look at the official docs.&lt;br&gt;
&lt;a href="https://reactjs.org/docs/state-and-lifecycle.html" rel="noopener noreferrer"&gt;React State and Lifecycle&lt;/a&gt;. Briefly, these are the methods which can be useful if you wanna execute a function when a component is mounted or unmounted or at every render of the component.&lt;/p&gt;

&lt;p&gt;But we cannot use any of the existing lifecycle methods (componentDidMount, componentDidUpdate, componentWillUnmount etc.) in a hook. They can only be used in class components. And with Hooks, you can only use in functional components. The line below comes from the React doc:&lt;/p&gt;

&lt;blockquote&gt;
&lt;h1&gt;
  
  
  &lt;em&gt;If you’re familiar with React class lifecycle methods, you can think of useEffect Hook as componentDidMount, componentDidUpdate, and componentWillUnmount combined.&lt;/em&gt;
&lt;/h1&gt;
&lt;/blockquote&gt;

&lt;p&gt;suggest is, you can mimic these lifecycle methods from a class component in a functional component.&lt;/p&gt;

&lt;h2&gt;
  
  
  ComponentDidMount
&lt;/h2&gt;

&lt;p&gt;Code inside &lt;strong&gt;componentDidMount&lt;/strong&gt; runs once when the component is mounted. useEffect hook equivalent for this behaviour is&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  // Your code here
}, []);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Notice the second parameter here (empty array). This will run only once.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  ComponentDidUpdate
&lt;/h2&gt;

&lt;p&gt;Without the second parameter the useEffect hook will be called on every render of the component.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  // Your code here
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  ComponentWillUnmount
&lt;/h2&gt;

&lt;p&gt;componentWillUnmount is used for cleanup (like removing event listeners, cancel the timer etc). Say you are adding an event listener in componentDidMount and removing it in componentWillUnmount as below.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;componentDidMount() {
  window.addEventListener('mousemove', () =&amp;gt; {})
}

componentWillUnmount() {
  window.removeEventListener('mousemove', () =&amp;gt; {})
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Hooks equivalent of the above code will be as follows&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;useEffect(() =&amp;gt; {
  window.addEventListener('mousemove', () =&amp;gt; {});

  // returned function will be called on component unmount
  return () =&amp;gt; {
    window.removeEventListener('mousemove', () =&amp;gt; {})
  }
}, [])
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Hope this was helpful 😄&lt;/p&gt;

</description>
      <category>react</category>
      <category>reacthooks</category>
    </item>
    <item>
      <title>Unlocking the Power of Redis: Storing Any Object as Cache in Ruby on Rails</title>
      <dc:creator>Aman</dc:creator>
      <pubDate>Tue, 28 Mar 2023 16:12:26 +0000</pubDate>
      <link>https://dev.to/onlyoneaman/unlocking-the-power-of-redis-storing-any-object-as-cache-in-ruby-on-rails-929</link>
      <guid>https://dev.to/onlyoneaman/unlocking-the-power-of-redis-storing-any-object-as-cache-in-ruby-on-rails-929</guid>
      <description>&lt;h3&gt;
  
  
  Unlock the full potential of Redis for storing any object as cache in Ruby on Rails. Learn how Redis works as a cache, how we can store classes and prevent threads fighting over single Redis connection.
&lt;/h3&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.amazonaws.com%2Fuploads%2Farticles%2Fdw6geki0b0a1bbvwjbq6.jpeg" 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%2Fdw6geki0b0a1bbvwjbq6.jpeg" alt="Unlocking the Power of Redis: Storing Any Object as Cache in Ruby on Rails" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Redis is an open-source, in-memory data structure store that can be used for caching, messaging, and real-time analytics. It has become increasingly popular in recent years because it is extremely fast, scalable, and easy to use. In this blog, we will be focusing on using Redis as a cache for storing any object in Ruby on Rails. We will explore the many benefits of using Redis as a cache and demonstrate how to integrate it into your Ruby on Rails application. We will also look into storing complex classes and arrays into redis efficiently, and finally how can we prevent different threads fighting over single redis connection.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is Redis?
&lt;/h3&gt;

&lt;p&gt;Redis stands for Remote Dictionary Server. It is an in-memory data structure store that is used as a database, cache, and message broker. It is an open-source project written in ANSI C. Redis supports a wide range of data structures such as strings, hashes, lists, sets, and sorted sets. Its amazing speed comes from its ability to keep the data in memory, which allows read/write operations to be performed with incredibly low latency.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Use Redis as a Cache?
&lt;/h3&gt;

&lt;p&gt;Caching is a mechanism for improving the performance of your application by storing frequently accessed data in memory. This reduces the number of times your application has to access the database, which in turn reduces the response time of your application. Redis is an excellent choice for caching because it is incredibly fast and can store any type of data. Redis is also very easy to use and can be integrated into your Ruby on Rails application with minimal effort.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Redis Works as a Cache
&lt;/h3&gt;

&lt;p&gt;Redis works by storing the data in key-value pairs in memory. When you request data from Redis, it checks if the data is already in memory, and if it is, it returns the data from memory. If the data is not in memory, Redis will fetch it from the database, store it in memory, and return it to you. This process is known as caching. When you modify the data, Redis will write the changes to the database and update the cached version in memory. This ensures that the cached version is always updated with the database.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to Use Redis in Ruby on Rails
&lt;/h3&gt;

&lt;p&gt;Using Redis in Ruby on Rails is very easy. First, you need to add the Redis gem to your Gemfile:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gem 'redis'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Next, you need to configure Redis in your environment file.&lt;br&gt;
Initialize a redis.rb file inside config/initializers with the below content.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;REDIS_CLIENT = Redis.new(url: "YOUR_REDIS_URL", timeout: 1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This configuration sets up Redis as the REDIS_CLIENT for your application. You can then use REDIS_CLIENT.set and REDIS_CLIENT.get to store and retrieve data from Redis. Here is an example of how to use Redis to cache data:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;data = {test: "hello"}
REDIS_CLIENT.set("test_key", data, ex: 60)
cached_user = REDIS_CLIENT.get("test_key")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;In this example, we are caching the object with an expiration time of 1minutes. When we retrieve the object from the cache, we get the cached version if it is still within the 1-minute expiration time, and if not, we fetch it from the database and store it in the cache again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Storing Objects (Arrays / JSON / other classes) in redis.
&lt;/h3&gt;

&lt;p&gt;Storing plain hashes or simple objects like strings, numbers is straightforward. But things get complicated as we try storing different classes or arrays using redis.&lt;/p&gt;

&lt;p&gt;E.g.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;data = User.first
REDIS_CLIENT.set("test_key", data, ex: 60)
cached_user = REDIS_CLIENT.get("test_key")
# Result -&amp;gt;  "#&amp;lt;V1User:0x000055e224d30520&amp;gt;"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Here, We need attributes like email, name, id associated with User to get stores in cache, but Redis stored the class instance simple. To avoid this, we need to convert such classes into attributes and pass a simple hash to Redis instead of Class.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;data = User.first
data_to_store = JSON.dump(data.attributes)
REDIS_CLIENT.set("test_key", data_to_store, ex: 60)
cached_user = REDIS_CLIENT.get("test_key")
proper_cached_user = JSON.parse(cached_user)
# Result -&amp;gt;   {"id"=&amp;gt;1, "email"=&amp;gt;"aman@gmail.com"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Similarly, there could be attributes which contain classes as a child, Let’s create a function which drills down below the object, converting it suitable to be stored in Redis.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def create_redis_cache
  key = "test_key"
  items = [User.first, User.second, {test: User.third}]
  items_to_store = attributes_from_item(items)
  REDIS_CLIENT.set(key, JSON.dump(items_to_store), ex: 60)
end

def get_redis_cache
  key = "test_key"
  items = REDIS_CLIENT.get(key)
  final_items = JSON.parse(items)
end

 def attributes_from_item(item)
    return item unless item.respond_to?(:attributes)
    new_item = {}
    item.attributes.each do |k, v|
      if v.is_a?(Hash)
        new_item[k] = attributes_from_item(v)
      elsif v.is_a?(Array)
        new_item[k] = v.map{|e| attributes_from_item(e)}
      else
        new_item[k] = v
      end
    end
    new_item
  end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;You can use the above create_redis_cache and get_redis_cache methods to store and retrieve cache with any data type without worrying about validity of cache.&lt;/p&gt;

&lt;h3&gt;
  
  
  Managing Concurrency
&lt;/h3&gt;

&lt;p&gt;When deploying rails, we often have a side worker like sidekiq, which runs parallel, doing tasks without disturbing the web application. When sidekiq opens many threads, your redis can start misbehaving due to the high number of open connections (sidekiq threads + puma threads) threads fighting over one connection (since the Redis Client only runs one command at a time, using a Monitor.). To solve this, You use a separate global connection pool for your application code.&lt;/p&gt;

&lt;p&gt;Add connection_pool to your Gemfile.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gem 'connection_pool'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;and then create limited pools in redis like this in your redis.rb initializer:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;require 'connection_pool'

REDIS = ConnectionPool.new(size: 10) { Redis.new, timeout: 1 }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This ensures that even if you have lots of concurrency, you’ll only have 10 connections open to memcached per Sidekiq process.&lt;/p&gt;

&lt;p&gt;Now in your application code anywhere, you can do this:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;REDIS.with do |conn|
  # some redis operations
  r = conn.get(redis_key)
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;You’ll have up to 10 connections to share amongst your puma/sidekiq workers. This will lead to better performance since, as you correctly note, you won’t have all the threads fighting over a single Redis connection.&lt;/p&gt;

&lt;p&gt;If you have survived till here, here is an &lt;a href="https://onlyoneaman.medium.com/how-to-make-rails-response-faster-a8cc5f1242d" rel="noopener noreferrer"&gt;article&lt;/a&gt; which will help you decrease your API Response time multi-folds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Advanced Features of Redis
&lt;/h3&gt;

&lt;p&gt;Redis is an incredibly versatile tool and has many advanced features that can be used to improve performance and functionality. Here are some of the most useful advanced features of Redis:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Pub/Sub&lt;br&gt;
Redis supports Publish/Subscribe messaging which allows you to set up a messaging system between your application and other external sources. This feature can be used to push data from your application to external sources or to receive data from external sources in real-time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Lua Scripting&lt;br&gt;
Redis supports Lua scripting which allows you to execute complex operations on the server-side. This can be used to perform complex calculations, transformations, or data manipulations.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Transactions&lt;br&gt;
Redis supports transactions which allow you to execute multiple commands in a single operation. This feature is useful when you need to ensure that a set of commands are executed atomically, meaning that either all of them are executed or none of them are executed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;TTL&lt;br&gt;
Redis allows you to set an expiration time for the data in the key-value pair. This is known as Time to Live (TTL). When the TTL expires, Redis automatically removes the key-value pair from memory. This feature can be used to reduce memory usage and to ensure that the cached data is always up to date.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;Redis is an excellent choice for caching frequently accessed data in your Ruby on Rails application. It is fast, scalable, and easy to use. Redis can store any type of data in memory and can be integrated into your application with minimal effort. It has many advanced features that can be used to improve performance and functionality. Using Redis as a cache can significantly improve the response time of your application and reduce the load on your database. Redis is a powerful tool that can help your application keep up with the demands of your users and stay ahead of the competition.&lt;/p&gt;

&lt;p&gt;Did the article help or anything else you would like to suggest or add? Add a response or drop a message below to me. 😉&lt;/p&gt;

&lt;p&gt;Follow me on &lt;a href="https://onlyoneaman.medium.com/" rel="noopener noreferrer"&gt;Medium&lt;/a&gt; for more articles. Also, Let’s connect if we haven’t yet &lt;a href="https://twitter.com/onlyoneaman" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/onlyoneaman/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  References
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://onlyoneaman.medium.com/how-to-make-rails-response-faster-a8cc5f1242d" rel="noopener noreferrer"&gt;&lt;strong&gt;How to Make Rails Response Faster&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/mperham/sidekiq/wiki/Advanced-Options#connection-pooling" rel="noopener noreferrer"&gt;&lt;strong&gt;Advanced Options&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/redis/redis-rb" rel="noopener noreferrer"&gt;&lt;strong&gt;GitHub - redis/redis-rb: A Ruby client library for Redis&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://api.rubyonrails.org/v7.0.4/classes/ActiveSupport/Cache/Store.html" rel="noopener noreferrer"&gt;&lt;strong&gt;ActiveSupport::Cache::Store&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://redis.io/docs/" rel="noopener noreferrer"&gt;&lt;strong&gt;Documentation&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
