embedding

Blog/·14 min

Your labels arrive every day. Fit and calibrate every day, measured against Jev 1.14

We ran the loop a customer would run: rows arrive, heads refit on a frozen reader, calibration refits on held rows, a versioned file ships, and we scored it after every batch against live Jev 1.14. Level on a bespoke workflow benchmark with a third less calibration error; eight points ahead on a 77-way intent stream with 92.5% of traffic automated at 91.5% precision. The ablation shows the one way to build this loop wrong, and the numbers say why fine-tuning is the wrong tool for data that keeps coming.


TL;DR

We built the loop a customer would actually run: labelled rows arrive over time, and every time a batch lands, vega refits one small head per question on a frozen reader, recalibrates on rows the heads never saw, writes a new versioned calibration file and serves it. Nothing is trained. Then we scored it after every batch against TypeSafe's Jev 1.14, live, on the same held-out rows.

Every table in this post, the per-round tables, the per-type breakdowns, the calibration tables at every confidence floor and the latency and cost lines, is in the full technical report (PDF), and the run itself is a public Kaggle notebook.

Why fine-tuning is the wrong tool for this job

Every decision model on the market learns your labels the same way: run a gradient through the language model until it agrees with you. Jev is a fine-tune. Prism is a LoRA on a 105B model. Our own laya checkpoints are fine-tunes too, and that is the better version of the idea, with reinforcement learning against a proper scoring rule. It is still the same idea.

Fine-tuning is a fine tool when the task is fixed, the data is all there, and someone pays for the GPU once. It is the wrong tool for a customer whose data arrives every day, for four reasons we have hit ourselves.

And one honest number. Fine-tuning has the higher ceiling when you have the data and the budget: our laya fine-tune on the same workflow benchmark reached 76.6%, above Jev and above the 4B fit. We are not claiming the loop beats a dedicated fine-tune on a fixed task. We are claiming it is the only one of the two you can leave running.

What fit and calibrate do instead

The reader is a frozen language model, 0.8B or 4B parameters here. It never sees a gradient. It turns a state and a question into a feature vector , and because the reader is frozen, that vector can be cached: a refit reads only the rows that are new since the last one.

Fit is one linear head per question. Standardise the features, project onto the top principal directions, then a multinomial logistic fit with an penalty:

That is convex, a few hundred steps on a tiny matrix, seconds on a CPU. The head is cross-validated and it is allowed to lose: it is kept only if its cross-validated accuracy beats chance, and it is served only if it also beats the frozen engine's own accuracy on the same rows. A head that learned nothing is discarded, and the loop tells you so.

Calibrate runs on rows the head never saw, and fits three numbers per question. A temperature that rescales the probabilities, found by minimising negative log-likelihood. A conformal threshold: with scores on the held rows, is the -th smallest, and the set of options with contains the right answer at least of the time on data like these rows. And a review line : the widest band of confidences above whose precision holds at the target, using Wilson's lower bound rather than the observed hit rate,

so that twelve for twelve on twelve rows does not count as a 100% precision band. If no line can hold the target, the band says so and routes everything to review. All of it is written to one JSON file stamped with the checksum of everything that changes an answer. That file is the deployment: every decision carries its version.

The deeper write-up of each step, with the theorems, is in Fit and calibrate, not fine-tune. This post is what happens when you run it as a loop against a hosted competitor.

The experiment

Two public streams, each played as arrivals. At each round, about a quarter of the arrived rows are held for calibration (chosen by a hash of the row id, so a row that calibrated at round three still calibrates at round four) and the rest fit the heads. After every round the same held-out set is scored. Jev 1.14 is called live on the same held-out rows, pinned to jev-1.14.0, with the dataset's own state and questions as the request body.

streamwhat it looks like in a businessarrivalsheld out
typed-decisionssupport threads, vendor invoices, security alerts, agent traces; five typed questions per record1,200 cases400 cases, 2,000 decisions
banking77one customer message, which of 77 banking intents4,928 rows924 rows, stratified

Jev has no fit and no calibration endpoint. What a customer gets is the cold model, so that is the headline comparison. We also measured the two things a customer could do with the same labelled rows: write them into Jev's criteria as examples, which is the only tuning surface its API has, and put Jev's answers behind our own calibration layer. Those come later in the post.

Stream one: a bespoke workflow taxonomy

Accuracy as labelled rows arrive on typed-decisions: vega 4B climbs from 0.445 to 0.737, vega 0.8B from 0.394 to 0.699, Jev 1.14 as shipped is a flat line at 0.747
Accuracy on the same 2,000 held-out decisions after every round. The dashed line is Jev 1.14 as shipped.
armrows usedaccuracyECEauto at 90% precisionprecision in band
Jev 1.14 as shipped00.7470.047no bandn/a
vega 0.8B cold00.3940.208no bandn/a
vega 0.8B, continuous1,2000.6990.04014.6%0.935
vega 4B cold00.4450.170no bandn/a
vega 4B, continuous3200.7160.0517.8%0.852
vega 4B, continuous6400.7410.03510.5%0.972
vega 4B, continuous1,2000.7370.03115.0%0.943

Read it as a customer would. Out of the box, Jev gets three in four decisions right. vega with no labels gets fewer than half. After the customer's own 1,200 rows have flowed through the loop, the 4B reader gets three in four as well. The gap between 0.737 and 0.747 is inside one standard error for 2,000 decisions, so we call it level and we do not call it a win.

Where it is not level is the number next to the answer. vega's calibration error is a third lower than Jev's, and vega can say which 15% of decisions are safe to automate at 94% precision. Jev returns probabilities but no band, so it cannot say that at all.

Expected calibration error as labelled rows arrive on typed-decisions: vega drops from above 0.2 to 0.031 once the calibration split has enough rows; Jev as shipped is at 0.047
Calibration error falls off a cliff at 320 rows: that is the round where the held-out calibration split first had enough rows per question.

One more thing the curve says. The returns halve with every doubling of rows: 320 to 640 bought four points, 640 to 1,200 bought none. The thing that moved this stream from 0.699 to parity was the reader, 0.8B to 4B, not more rows. On a frozen reader, labels can only sharpen what the features already separate.

Stream two: 77 intents, thousands of messages

Accuracy as labelled rows arrive on banking77: vega 4B climbs from 0.601 to 0.875 at 4,928 rows and crosses Jev 1.14 as shipped at 0.794 between 1,232 and 2,464 rows
The crossing. vega passes Jev as shipped between 1,232 and 2,464 rows and keeps climbing.
armrows usedaccuracyECEauto at 90% precisionprecision in band
Jev 1.14 as shipped00.7940.078no bandn/a
vega 4B cold00.6010.251no bandn/a
vega 4B, continuous6160.6290.0522.6%1.000
vega 4B, continuous1,2320.7740.04661.0%0.922
vega 4B, continuous2,4640.8500.03886.7%0.906
vega 4B, continuous4,9280.8750.02892.5%0.915

This is the stream the loop is for. Short messages, many categories, and a customer who has thousands of them. The band opens as the calibration rows accumulate: 2.6% automated at 616 rows, 61% at 1,232, 92.5% at 4,928, and the precision inside the band stays at or above the 90% it was asked for every time. The dip at 308 rows is real: four rows per intent is enough to pass cross-validation and not enough to beat the engine on every intent, and the harness served the head anyway because on average it won. By 616 rows it had recovered.

What that means on a desk. Take a bank that gets 100,000 messages a day. With this file, 92,500 are answered automatically and about 7,900 of those are wrong, which is the 91.5% precision; 7,500 go to a person with a reason attached. With Jev as shipped you get 79,400 right and 20,600 wrong, and no instrument to pick the subset you would trust, because there is no band. The accuracy gap is eight points. The operational gap is that one of the two can be put on autopilot and the other cannot.

The ablation, and what it means for the business

Every arm below starts from the same arrived rows at the final round, 4B reader, both streams. This is the table to show anyone who wants to build a self-improving loop, because it contains the way to build it wrong.

armtyped-decisions accECEautobanking77 accECEauto
engine cold, nothing fitted0.4450.170n/a0.6010.251n/a
engine + calibrate, no heads0.4450.0534.2%0.6010.09114.3%
heads, no calibrate0.7370.124n/a0.8750.243n/a
heads + calibrate on the fitting rows0.7370.258100%0.8750.051100%
heads + calibrate on held rows (the product)0.7370.03115.0%0.8750.02892.5%
heads on every row, no calibration split0.7530.117n/a0.8850.245n/a

Four things fall out of it.

What Jev can do with the same rows

Its API has no fitting surface, so the labelled rows can only go into the request. We wrote up to two state excerpts per option into each option's criteria (the tuned arm), and separately we fitted our own temperature, conformal threshold and band on Jev's answers to the calibration rows (the layer arm).

armtyped-decisions accECEautobanking77 accECEauto
Jev as shipped0.7470.047no band0.7940.078no band
Jev + examples in criteria, 80 / 308 rows0.7520.034no band0.8570.042no band
Jev + examples in criteria, all rows0.7460.026no band0.8570.042no band
Jev + our calibration layer0.7460.04432.1% at 0.9460.7940.06864.9% at 0.940
vega 4B, continuous (for reference)0.7370.03115.0% at 0.9430.8750.02892.5% at 0.915

Examples in the prompt help Jev on the intent stream, six points, and stop helping after the first few hundred rows; on the workflow benchmark they do nothing. They also multiply the tokens per call by five, forever. The layer arm is the one we find interesting: our calibration file works on someone else's model. Jev behind it gets a band it never had, 65% of banking traffic automated at 94% precision. If you already pay for a hosted decision model, this is the part you can add without changing anything else.

Cost and time, measured

itemmeasured
Jev, all arms, both streams, 4,673 live calls$0.65 at the published $0.042 per million input tokens
Jev per million decisions at this benchmark's token countsabout $8
Jev request latency, median, from Kaggle85 to 97 ms
vega 4B refit at 1,200 arrived rows on one T420 minutes, almost all the engine cross-check
vega 4B, whole study on two T4s, both streams in parallel2 h 40 min
vega 0.8B feature read on an Apple M565 ms, cached after the first read

So at benchmark scale cost is a rounding error on both sides, and we are not going to pretend otherwise. The refit time is worth a note: reading new rows is proportional to the arrival rate, but the engine cross-check that decides whether a head is worth serving reads every fitted row once. That is the one cost in the loop that grows with history, and it is the next thing to cache.

Where this leaves a buyer

If you have a bespoke workflow with a few hundred to a thousand labelled records, a frozen 4B reader with the loop matches a hosted decision model on accuracy, beats it on calibration, and is the only one of the two that tells you what to automate. If you have a stream of short messages with many categories and thousands of labels, the loop passes the hosted model and keeps going. In both cases the rows never leave your network, there is no meter, and every decision names the calibration version that made it.

What we are not saying: that the loop beats Jev on the workflow benchmark, because it does not, and that more rows would fix that, because the curve says they would not. The levers there are the reader, the 27B being the next one up, and the head itself. We will publish those runs when we have them, on the same held-out rows, with the same cached Jev answers, so the comparison stays honest.

Run the loop yourself

The loop is three calls in the public package. Everything the harness added on top was the arrivals, the hold-out by id, and the scoring.

pip install -U vegaml

import vegaml
from vegaml.calibrate import calibrate

V = vegaml.load("4b")                        # or vegaml.load() for the 0.8B

def held(row_id):                            # about a quarter of rows, by id, never by arrival order
    import hashlib
    return int(hashlib.sha1(row_id.encode()).hexdigest(), 16) % 4 == 3

def refit(arrived, questions, version):
    fit_rows = [(s, labels) for rid, s, labels in arrived if not held(rid)]
    cal_rows = [(s, labels) for rid, s, labels in arrived if held(rid)]
    report = V.fit(fit_rows, questions, folds=5)                # heads; each one allowed to lose
    cal = calibrate(V, cal_rows, questions, alpha=0.1,          # temperature, conformal threshold,
                    target_precision=0.90)                      # review band at 90% precision
    cal.save(f"calibration_{version}.json")                     # stamped with its checksum
    return report, cal

# every batch: arrived grows, refit runs, the new file serves
out = cal.apply(V.decide(state, questions))                     # probs, prediction_set, abstain, route

The full run is a public Kaggle notebook: kaggle.com/code/nandakishorconvai/vega-fit. Two T4s, typed-decisions on one GPU and banking77 on the other, the Jev key in a secret named JEV_API, and it writes the same results.json and per-round calibration files the report was built from. The datasets are LocalLLaMA/typed-decisions and PolyAI/banking77, so every number above can be checked line by line.


More posts·embedding.in