Last month I saw the bonkers $870M TypeSafe series A, and honestly was a little dumbfounded. I remember talking about this "safe LLM output" strategy a couple years ago, and the speedrun arrival of legitimately competitive clones mere days after release supported this idea that the problem was "not that insightful". Later, I realized their core bet wasn't necessarily "provide an accurate classifier", but probably more like "reduce MLE load" and "expand market for classification" thanks to implementation ease. Those insights came from me having built out taggit.io 7 years ago, a text classifier platform enabling anyone to train and use text classifiers. (alas, The Market Wasn't Ready™)

After going through the N stages of grief seeing something succeed spectacularly there (and coming out the other side), I realized that a) the scaled text classification problem was still interesting, b) the state of the art had moved since I last looked, and c) there was probably something interesting to explore at the intersection there. This post is a walk through that garden, narrating the space, my findings, and presenting a little demo.

tl;dr

  • Decision models are essentially zero-shot classifiers. By capturing and labeling data, you can perform much better and much cheaper.
  • A 4B fine-tune (a LoRA) achieves 0.91 macro F1 @ $6.10 per 1M decisions (vs 0.77 OpenAI @ $19.20 per 1M). A 33M classifier distilled from the LoRA achieves 0.894 macro F1 @ $0.09 per 1M. (evaluated on the taggit dataset)
  • Decision on strategy is dominated by value of classifier accuracy vs volume. Most should prototype with the decision model, and plan for the capture + label loop as the product demonstrates value.
  • calibrate.sao.dev demos how to do a labeling loop (label capture is private, model exports free)

Decision Models / Classifiers

The decision model, an LLM sampling output probabilities for one token to represent answer class, is essentially a zero-shot classifier. Traditionally, classifiers were trained from scratch on labeled data, learning everything they knew about the task from it. Later, for well-resourced domains (text, images, etc), we realized we could pre-train most of the network on unlabeled data, so a classifier starts out knowing the language and only has to learn the task: e.g. a pre-trained encoder with a logistic layer on top, still a very common text classifier recipe. I'd put the decision model at this spectrum's extreme: it relies entirely on what the pre-trained model already knows, plus the LLM's in-context learning to adapt to each problem.

A towering 4B decision model mech and a small 33M classifier mech point at each other in a hangar, both saying "p(yes) 0.81"

If you squint, the classifier fills an interesting role in our products and systems: it adapts a squishy, subjective domain into a simple categorical domain local to the formalisms of the business/product/etc. It turns arbitrary text into a "safety/trust risk" flag, or an image into "there is an apple at these coordinates". In a significant way, the ability to create, maintain, and improve classifiers is the ability to adapt a real-world problem into a software solution, and create a product around it.

On our path to discussing the right strategy per use case in the labels/classifiers/decision-model space, we're going to discuss some important concepts that support the decision:

  1. Accuracy is Incomplete - and the metrics you should be looking at for classifiers.
  2. Labels are Critical - the core roles that labeling plays in any system doing classification.
  3. Comparing Strategies - placing available strategies in the (setup cost, inference cost, classifier quality) space.
  4. Bootstrapping - decision models as the on-ramp to "real" classification systems.
  5. Demo - a working labeling loop you can use today: describe a classifier, label what it asks, export it as a container.

Accuracy is Incomplete

In classification, accuracy is a very incomplete metric, especially under class imbalance - and almost all classifiers have non-trivial class imbalance. If you're looking for vibes-similar quality metrics, you're probably looking for precision, recall, and F1 score; wikipedia has a great explainer for them. Borrowing their phrasing: Precision is the ratio of positively classified instances that were true positives: TP / (TP + FP); and Recall is the ratio all positive instances which were classified as positive: TP / (TP + FN). F1 score is the harmonic mean of precision and recall, a single number that's only high when both are. Every table in this post reports macro F1: the F1 of each class (here, "yes" and "no"), averaged, so a classifier can't score well by ignoring the rare class.

ALWAYS NO
PREDICTS NO FOR EVERYTHING
ACCURACY
95%
PRECISION
—
RECALL
0.00
F1
0.00
USEFUL
CATCHES MOST, SOME FALSE ALARMS
ACCURACY
93%
PRECISION
0.40
RECALL
0.80
F1
0.53
CAUGHTMISSEDFALSE ALARMCORRECT NO

Let's take 200 items where 10 are positive. A classifier that always says "no" is 95% accurate, but catches nothing: its macro F1 is 0.49. One that learns something and catches 8 of the 10 at the cost of 12 false alarms is less accurate at 93%, but its macro F1 is 0.75, and it's actually worth shipping. The taggit data in this post has the same shape: always answering with the majority class scores 0.61 accuracy, but 0.37 macro F1.

Another dimension missed by both accuracy and P/R/F1 is calibration. A classifier is calibrated when its confidence means what it says: of the answers it gives at 80%, about 80% are right. That's what makes a 0.54 useful rather than noise: you can route it to a person for review, auto-accept the 0.98s, and set those thresholds knowing what they'll cost you. (Calibrated doesn't mean often unsure; a very accurate model can put nearly everything near 0 or 1, and be right to.) To my pleasant surprise, people have actually been talking about calibration with decision models. In my experience building classifiers at magnificent-seven scale, consumers often "just want the answer"; the tide of ML education seems to be rising with LLMs.

AUTO NO REVIEW AUTO YES CALIBRATED 10 IN REVIEW · 1 CONFIDENT MISS OVERCONFIDENT 1 IN REVIEW · 4 CONFIDENT MISSES 0 0.3 0.7 1
TRULY YESTRULY NOWRONG BUT CONFIDENT

The standard measure is Expected Calibration Error (ECE): group answers by confidence (say into 10 bins), compare each bin's average confidence to how often it was actually right, and average the gaps, weighted by how many answers fall in each bin. 0 is perfectly calibrated.

HOW ECE IS COMPUTED

Bin every prediction by its score, then compare each bin's average score to how often its items were truly yes. A calibrated classifier lands on the diagonal; ECE is the average gap, weighted by how many predictions fall in each bin.

0 0 0.25 0.25 0.5 0.5 0.75 0.75 1 1 FRACTION TRULY YES PREDICTED P(YES), 10 BINS CALIBRATED · ECE 0.02 OVERCONFIDENT · ECE 0.11 PERFECT

Here's how the strategies in this post measure up on the taggit holdouts:

Reliability: each strategy's confidence in its answer against how often it was right, pooled over 3,671 holdout labels in 10 bins, one strategy shown at a time; points on the diagonal are calibrated, and a point's area is its share of the labelsOpenAIzero-shotembeddingfew-shotdistilledLoRA0.30.50.70.90.50.60.70.80.91.0calibrated: right as often as it saysconfidence in its answer (10 bins)accuracyOpenAI decisions: 440 labels at confidence 0.52, accuracy 0.34OpenAI decisions: 166 labels at confidence 0.64, accuracy 0.58OpenAI decisions: 195 labels at confidence 0.75, accuracy 0.57OpenAI decisions: 364 labels at confidence 0.85, accuracy 0.73OpenAI decisions: 2,506 labels at confidence 0.98, accuracy 0.92OpenAI decisions: ECE 0.089Decision model, zero-shot: 441 labels at confidence 0.55, accuracy 0.55Decision model, zero-shot: 438 labels at confidence 0.65, accuracy 0.68Decision model, zero-shot: 565 labels at confidence 0.75, accuracy 0.77Decision model, zero-shot: 1,170 labels at confidence 0.86, accuracy 0.93Decision model, zero-shot: 1,057 labels at confidence 0.92, accuracy 0.98Decision model, zero-shot: ECE 0.049Embedding classifier: 281 labels at confidence 0.55, accuracy 0.57Embedding classifier: 325 labels at confidence 0.65, accuracy 0.68Embedding classifier: 386 labels at confidence 0.75, accuracy 0.77Embedding classifier: 584 labels at confidence 0.86, accuracy 0.86Embedding classifier: 2,095 labels at confidence 0.97, accuracy 0.96Embedding classifier: ECE 0.016Decision model, few-shot k=4: 267 labels at confidence 0.55, accuracy 0.54Decision model, few-shot k=4: 266 labels at confidence 0.65, accuracy 0.75Decision model, few-shot k=4: 378 labels at confidence 0.75, accuracy 0.76Decision model, few-shot k=4: 776 labels at confidence 0.86, accuracy 0.91Decision model, few-shot k=4: 1,984 labels at confidence 0.94, accuracy 0.98Decision model, few-shot k=4: ECE 0.040Distilled classifier: 97 labels at confidence 0.55, accuracy 0.59Distilled classifier: 71 labels at confidence 0.66, accuracy 0.54Distilled classifier: 114 labels at confidence 0.75, accuracy 0.69Distilled classifier: 226 labels at confidence 0.86, accuracy 0.73Distilled classifier: 3,163 labels at confidence 0.98, accuracy 0.96Distilled classifier: ECE 0.031Decision model, fine-tuned (LoRA): 75 labels at confidence 0.55, accuracy 0.53Decision model, fine-tuned (LoRA): 71 labels at confidence 0.65, accuracy 0.61Decision model, fine-tuned (LoRA): 94 labels at confidence 0.75, accuracy 0.77Decision model, fine-tuned (LoRA): 190 labels at confidence 0.86, accuracy 0.76Decision model, fine-tuned (LoRA): 3,241 labels at confidence 0.98, accuracy 0.97Decision model, fine-tuned (LoRA): ECE 0.019

The models trained on labels are the calibrated ones (ECE 0.016–0.031). The zero-shot decision model is underconfident (right more often than it says), and OpenAI decisions are overconfident. So, two numbers: macro F1 for how often a classifier is right on both classes, and ECE for whether you can act on its confidence.

Labels are Critical

The really challenging part about classification in general is that once you really get into it, classification tasks can get very nuanced. Even a subtle change in phrasing can change the output class, and in ways even relatively high capacity decision models don't catch. Labeled data is the way to handle this: for many nuances of the classification, it is simply more effective to show than tell. In industrial labeling programs, human labelers are given both highly refined narrative text and instructive examples demonstrating nuances. The same is necessary for classifiers trained to solve the same problem.

Every strategy tested as part of this work (other than few-shot) improved with labels: (some more than others)

Mean macro F1 against human labels, one line per tier, over the models with more training labels than the count (47 at 50 and all; fewer at 100, 200 and 400, counted under the axis); the dashed line is the 4B zero-shot floor's mean over all 47; the all column, every model at its own training size, is drawn apart as ticks since it is not the next point on the curvemacro F10.60.70.80.91.05047100442003140011all47human labels (log scale), models averaged belowall is each model at its own size4B zero-shot floor, mean of 47: F1 0.814embedding classifier, mean of 47: F1 0.689 at 50 labelsembedding classifier, mean of 44: F1 0.754 at 100 labelsembedding classifier, mean of 31: F1 0.826 at 200 labelsembedding classifier, mean of 11: F1 0.853 at 400 labelsembedding classifier, mean of 47: F1 0.821 at all labelsfew-shot k=4, mean of 47: F1 0.875 at 50 labelsfew-shot k=4, mean of 44: F1 0.872 at 100 labelsfew-shot k=4, mean of 31: F1 0.869 at 200 labelsfew-shot k=4, mean of 11: F1 0.894 at 400 labelsfew-shot k=4, mean of 47: F1 0.872 at all labelsdistilled classifier, mean of 47: F1 0.842 at 50 labelsdistilled classifier, mean of 44: F1 0.875 at 100 labelsdistilled classifier, mean of 31: F1 0.902 at 200 labelsdistilled classifier, mean of 11: F1 0.914 at 400 labelsdistilled classifier, mean of 47: F1 0.894 at all labelsLoRA, mean of 47: F1 0.860 at 50 labelsLoRA, mean of 44: F1 0.894 at 100 labelsLoRA, mean of 31: F1 0.927 at 200 labelsLoRA, mean of 11: F1 0.931 at 400 labelsLoRA, mean of 47: F1 0.912 at all labelsLoRAdistilled classifierfew-shot k=4embedding classifierzero-shot decision modelembedding classifierfew-shot k=4LoRAdistilled classifier

Categorically, label sets fall into two purposes: improving the classifier, or measuring the classifier's quality metrics.

For improving the classifier, you want a base of representative data for the common cases/themes/etc, but you also want representation in the training set for the details that make the decision boundary complex. There are pockets of examples that are hard, but show the nuance from phrasing, word-sense disambiguation, etc. They have much higher impact on net classifier quality than another basic example does. You'd source examples to label for this purpose via different sampling strategies: random sampling, corrections by domain operators, and from low-confidence answers by your existing classifier (another reason calibration is important). These examples are not suitable for calculating the expected future performance of the model, though, since a) the sampling process doesn't match reality, and more seriously, b) they were chosen precisely because a classifier got them wrong (active learning), so they're harder than what the classifier will usually see.

This is one nit on the measurements in this blog post: the taggit data was labeled this way, so difficulty rises over time: later labels were chosen mostly because a classifier got them wrong. Since the evaluation holds out each model's latest labels, the quality numbers here are very likely underestimates.

For quality metrics, you want to construct a sampling process that produces a representative sample from your domain continuously, so you can have something label it (a human, or a higher capacity model if you're cheap), and then use those labels to identify quality regressions or performance holes. The world and language legitimately does change, and thus so does the data distribution - this is why no classifier over human-generated data is ever complete. You might choose to do stratified sampling to ensure fidelity across categories. Generally its worth a human being involved (at least in part) in the ongoing labeling loop, as it is generally where new classes of hard examples are identified.

There is essentially one case where you don't need a labeling loop set up, and that's the low-volume regime where vanilla decision models are accurate enough. This is a sweet spot where renting what a hosted decision model already knows makes a lot of sense. For essentially all other cases, you want a labeling loop:

Comparing Strategies

In my walk around the space I explored a few categories of strategies:

Sorted by F1, the table reads as one lesson: model size sets the cost, and how much of the model learned your task sets the quality.

StrategyModelLearns your task fromCost / 1MMacro F1vs LoRA (F1, cost)
OpenAI decisionsundisclosed, hostednothing (instructions only)$19.20 billed0.766−0.146 F1, 3.1× cost
Decision model, zero-shot4Bnothing$5.67 GPU0.814−0.097 F1, 0.93× cost
Embedding classifier33Mlabels, last layer only$0.09 CPU0.821−0.091 F1, 1/67× cost
Decision model, few-shot k=44Bits 4 nearest labels, in the prompt$19.69 GPU0.872−0.040 F1, 3.2× cost
Distilled classifier33Mlabels + the LoRA's answers, whole model$0.09 CPU0.894−0.018 F1, 1/67× cost
Decision model, fine-tuned (LoRA)4Blabels, whole model (via an adapter)$6.10 GPU0.912reference

Orange is the best of each numeric column; for vs LoRA (F1 behind it, cost as a multiple of its) the least F1 given up under $1 per 1M. Click an F1 for accuracy, macro precision and recall. Few-shot neighbours come from the model's own training labels (median 283) and add 484 prompt tokens per decision. OpenAI refused 231 of 3,671 holdout labels, each scored as a uniform guess; on the rest it scores 0.808.

Distilled classifier is the embedding classifier's encoder fine-tuned end to end on the labels plus the LoRA's probabilities for 4,000 nearby unlabeled spans: 66 MB of weights per classifier, where the embedding classifier's layer is a few KB on a shared encoder.

Latency (p50): OpenAI decisions 132 ms (request wall time, 8 in flight); Decision model, zero-shot 24.2 ms (L4 batch throughput); Embedding classifier 3.58 ms (one unbatched decision, all threads); Decision model, few-shot k=4 81 ms (L4 batch throughput); Distilled classifier 7.44 ms (one unbatched decision, one core); Decision model, fine-tuned (LoRA) 26 ms (L4 batch throughput). These answer different questions and are not ranked.

Rates. OpenAI: $0.10 per 1M billed input tokens (decisions guide, checked 2026-10-10). GPU: measured L4 seconds at Modal's $0.000222/s (modal.com/pricing, checked 2026-10-11), GPU kept busy, steady state; each vLLM container start adds 260 s, $15.72 per 1M at this run's size. CPU: the embedding classifier is 6.9 ms per decision on one thread (Ryzen 9 7950X3D, 16 vCPU; its p50 above used all threads), so 6900 core-seconds per 1M at Modal's $0.0000131 per physical core-second (modal.com/pricing, checked 2026-10-17), memory excluded. The distilled classifier runs the same graph and is priced the same; its own export timed 7.44 ms, inside this box's run-to-run spread.

OpenAI's hosted model only reads instructions and scores lowest; a 33M model that learned the whole task from a teacher lands within 0.02 of the 4B LoRA at 1/67 of the cost. Plotting cost against quality, the frontier is short:

Cost per 1M decisions against mean macro F1 over all 47 models at three human-label budgets (0, 50, all), one shown at a time; filled points and the step line are the Pareto frontier0 labels50 labelsall labels (83–1,201)frontierdominatedmacro F10.70.80.91.0~$0$0.1$1$10majority: macro F1 0.372, ~$0 per 1Mmajority 0.372 (off scale)zero-shot decision model: macro F1 0.814, $5.67 per 1Mzero-shot decision model 0.814OpenAI: macro F1 0.766, $19.20 per 1MOpenAI 0.766distilled classifier: macro F1 0.842, $0.09 per 1Mdistilled classifier 0.842embedding classifier: macro F1 0.689, $0.09 per 1Membedding classifier 0.689few-shot k=4: macro F1 0.875, $19.69 per 1Mfew-shot k=4 0.875LoRA: macro F1 0.860, $6.10 per 1MLoRA 0.860zero-shot decision model: macro F1 0.814, $5.67 per 1Mzero-shot decision model 0.814OpenAI: macro F1 0.766, $19.20 per 1MOpenAI 0.766distilled classifier: macro F1 0.894, $0.09 per 1Mdistilled classifier 0.894embedding classifier: macro F1 0.821, $0.09 per 1Membedding classifier 0.821LoRA: macro F1 0.912, $6.10 per 1MLoRA 0.912few-shot k=4: macro F1 0.872, $19.69 per 1Mfew-shot k=4 0.872zero-shot decision model: macro F1 0.814, $5.67 per 1Mzero-shot decision model 0.814OpenAI: macro F1 0.766, $19.20 per 1MOpenAI 0.766$ per 1M decisions, log scale

While it's tempting to construct a 2x2 here, the real process for non-MLEs (or busy MLEs) is to bootstrap with a decision model, then choose the strategy that best fits your requirements. If that's maximizing precision/recall, you're going to LoRA; for high scale at a reasonable price, it's a lower capacity model like the distilled classifier here, 0.02 F1 behind the LoRA at 1/67 of the cost (its cost moves to 66 MB of weights per classifier, and a LoRA to train as its teacher). Few-shot only makes the frontier at 50 labels, by a noise-sized margin, at three times the LoRA's price.

Bootstrapping

One thing that decision models get right as a product is giving you something useful from the get go. In the classic classifier paradigm, you need to label a bunch of data, write your text classifier software, host the model, and then you get to start making decisions based on its results and building the broader feature. With decision models, you literally just make an API call, and get something useful back. It's not the most accurate, but it proves feature value much sooner than a hand-built classifier.

Looking back on Taggit, I think this is actually a key insight: MLEs don't need help creating a classifier, they know how to do it already; non-MLEs who don't know how to create a classifier also don't have an intuition for when they are valuable, their costs, etc. This means they can't go from 0 to 1 in many cases, and can't know if the juice is worth the squeeze.

This makes the likely path for most recent adopters of decision models something like:

calibrate.sao.dev

Having built a system like this before, I figured a demo would be helpful. There are two ways to try it:

  1. The extended demo at calibrate.sao.dev, where you can create your own classifier, bootstrap it, and use it.
  2. The embedded demo, where you can try out a small classifier I built out.

The purpose of the extended demo is to show that the labeling loop and bootstrapping part of building a classifier is actually not that much work, especially with a coding agent in the loop. Once you create a classifier, it will grill you with positive and negative examples, and then train an embedding classifier for you. (not the distilled one 1, to make hosting easier on me) You can then export this classifier as a docker image, allowing you to host inference on your own infra. Classifications also come with a correction link alllowing you to keep improving the classifier on calibrate.sao.dev if you want!

You can try out the in-browser example classifier for the app-crash topic from the taggit dataset:

TRY IT

A public classifier from the calibrate demo, run in your browser.

1

Doing this yourself with a coding agent should be pretty straight forward - just point it at this blog post and ask it to implement it for you.




Appendix

The data

Dataset
Models47
Holdout labels (latest 20% per model)3,671
Training labels (earliest 80%)14,781 in all; per model median 283, min 83, max 1,201
Options per question47 binary
Majority-class baselineaccuracy 0.607, macro F1 0.372

By model name: 14 app-*, 7 clothing-*, 3 podcast-*, and 23 others (ad-annoyance, anger, anticipation, bad-service, checkout, cleanliness, …): app, product and podcast review complaints, service quality and sentiment. Each model is one human-labeled taggit question over a tagged span of review or customer text. Means are over models unless marked pooled. Text, label sources and per-label records live on the calibrate-systems share and are not summarised here.

Calibration and refusals

What the costs mean

The label curve

How many nearest examples

kMacro F1ECE (pooled)Prompt tokens / decisionGPU $ / 1Mp50 ms
00.8140.049not recorded$5.6724.2
40.8720.040484$19.6981
160.8790.0391,567$65.20267

Neighbours are each text's k nearest training labels by bge-small cosine, as prior chat turns, nearest last. GPU cost is steady state on an L4, latency batch throughput; every example is prefilled on every decision, so cost grows with k.

Distillation and student capacity