DEV Community

Cover image for Jev, the model that cannot write a word, and where it fits in web scraping (does it?)
Ayan Pahwa for Extract by Zyte

Posted on Originally published at zyte.com

Jev, the model that cannot write a word, and where it fits in web scraping (does it?)

My first reaction to Jev was that I did not get it, and I suspect I was not the only one. You give it something, you ask a question, and it comes back with yes or no and a confidence number. This is where AI started for me. Is this a photo of a dog? Yes, 92% confident. That demo is older than most people's careers, so when the launch thread was filled with people calling it a new category of model, I assumed I was missing the joke. Rather than keep arguing with a comment section, I spent an afternoon putting it into the scraping workflow I actually use.
The part that makes Jev odd is that it cannot write. It does not write badly or write short; it has no ability to produce a string at all. You hand it data and a list of typed questions, and it hands back probabilities and choices which could be decision directions your agents can take, so my first thought was to use it as a complimentary block with LLMs for agentic application use-case to increase accuracy of output or to reduce cost for my agents. More on this later.
Initially it also sounded useless for web scraping, since extraction means producing text. That turned out to be the wrong way around, because there is a job in every pipeline that is not extraction at all: deciding whether the record you just built is any good.

What the hype is about

TypeSafe AI put Jev on Hacker News on September 15, 2026, and the launch thread reached more than 1,900 points and 509 comments. Three days later a project called OpenJev drew 714 points of its own.
The appeal is easy enough to state. Output tokens are free, the answer always comes back in the shape you asked for, and a per-record call is quick enough not to be the bottleneck.

What it is

Jev belongs to a category TypeSafe calls System One models. An ordinary language model is autoregressive: it predicts a token, appends it to the context, predicts the next one, and repeats until it decides to stop, which is why output costs money.
Jev is not trained to generate text.
You give it a state, which is your data, and a map of typed questions, and it evaluates every question against that state in parallel. TypeSafe describes what comes back as typed decisions and probabilities rather than generated text. The answer to your fifth question is not waiting on your first.
A generative model emits tokens one at a time in a chain, while Jev evaluates several typed questions in parallel
Everything you can ask is one of three shapes. A Noul is a yes or no question returning a probability between 0 and 1, and it has no confidence field, which catches people out: the probability is the answer, not a measure of how sure the model is. A Choice picks one option from a set you define, up to 255 of them, and returns the winner, the distribution, and a confidence. A Score places the input on a rubric you write, and its number is probability-weighted across your levels, so it can land between two rungs.

How it works, in the simplest case

A request is one flat JSON object, and the answers come back under keys you chose yourself:

{
  "state": "Help! My payouts have been failing for three days.",
  "model": "jev-latest",
  "questions": {
    "is_urgent": { "type": "noul", "instructions": "Does this convey urgency?" },
    "team": {
      "type": "choice",
      "instructions": "Which team should handle this?",
      "criteria": {
        "billing": "Payments, invoicing, refunds",
        "technical": "Bugs, outages, integrations"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

You get is_urgent back as a float and team as one of your two strings plus confidence. No parsing, no retry loop, no prompt engineering to coax valid JSON out of a model that would rather write a paragraph.

How it differs from a language model, and what it costs you

Output tokens are free. Input runs at $0.042 per million tokens and everything the model produces costs nothing, which inverts the usual incentive: with a generative model you ask the fewest questions you can get away with, and here you ask everything you might want to know. The answer is always a valid instance of the type you requested. Responses land inside two seconds. TypeSafe says the probabilities are calibrated; I did not test that, and one result below makes me want to.
The launch coverage mostly stops there. The next bit matters more. You will always get a well-formed answer, it can be completely wrong, and arriving in a tidy shape makes it easier to trust than it has earned.
Against that, it cannot generate a string, so it can never fill in a field for you. TypeSafe's own model jaggedness page is blunt about the consequence: "For data extraction, it is better to extract possible options using regex or a generative model and let jev-1.13 pick the correct extraction." Your code proposes, the model decides.
A Choice always returns one of the options you gave it, and the confidence does not reliably warn you when none of them fit: in a larger run it labeled a category listing page poetry at a confidence of 1.00.
The documentation tells you to add an other or none of the above options for exactly that reason, and I did not, which you will see the cost of below. Context is capped at 64k tokens per request, with 32k for the state plus your longest question. It is text only, English first, and the documentation is honest that CJK scripts are handled but not equally well.

Where it could fit in web scraping

My filter, after getting this wrong twice: if a regular expression, a status code, or a CSS class can answer the question, do not ask a model. Those signals are structural, and code beats a non-deterministic model on structure every time, for nothing.
What is left is the questions where the answer only exists in the language, and where the alternative is a hand-maintained list of phrases that is never finished. Classifying a product into a category, or deciding whether a description actually describes the thing it is attached to, are not regex problems, and I have a number for that claim further down.
A gate runs after the work, on a record you have already built, and decides whether to trust it. A switch runs before, and decides what the pipeline does next, so it is asking ahead of the expensive thing instead of auditing after it. Switches are the more interesting group if cost is what you care about, and they are not unique to scraping: routing a support ticket to a person or a canned reply, sending an uploaded document to the right parser, and deciding whether a log line is worth waking anyone over are all the same shape.
A gate runs after the work and decides whether to trust a record. A switch runs before it and picks a cheap branch, an expensive branch, or skipping the page
The scraping switch I keep coming back to is choosing an extraction type, because Zyte API will not let you combine multiple automatic extraction fields in one request, so something upstream perhaps could decide between product, article, job posting, and the rest before you spend the call. I have not tested that one yet, and my own filter argues against it: most sites announce their page type structurally, in the URL, in JSON-LD @type, or in an og:type tag, and code should read those first. A Choice earns a look only for the pages where none of that is present.

One real example

The shape I ended up with is the gate, and it is about as plain as it looks:
Sequence diagram: fetch the page with requests, parse with BeautifulSoup, run cheap checks, then one batched call of six questions to Jev before writing or holding the record
I scraped a single book from books.toscrape.com, using nothing but requests and BeautifulSoup:

import re
import requests
from bs4 import BeautifulSoup
def scrape(url):
    html = requests.get(url, timeout=30).text
    soup = BeautifulSoup(html, "html.parser")
    cells = [c.get_text(strip=True) for c in soup.select("table.table-striped td")]
    heading = soup.select_one("h1")
    desc = soup.select_one("#product_description ~ p")
    price = cells[2] if len(cells) > 2 else None
    return {
        "name": heading.get_text(strip=True) if heading else None,
        "author": None,                       # the site does not publish one
        "description": desc.get_text(strip=True) if desc else None,
        "price": re.sub(r"[^\d.]", "", price) if price else None,
        "currency": "GBP" if price and "£" in price else None,
        "category": soup.select("ul.breadcrumb li a")[-1].get_text(strip=True)
                    if len(soup.select("ul.breadcrumb li a")) > 1 else None,
        "availability": cells[5] if len(cells) > 5 else None,
    }
Enter fullscreen mode Exit fullscreen mode

Those if heading else None guards are not decoration. Point these selectors at a page that does not exist and they all come back empty, and without the guards the parse raises before you reach the interesting part.
Then one call to Jev API carrying six questions about that record at once:

QUESTIONS = {
    "is_a_book":      {"type": "noul", "instructions": "Is this record a single real book, rather than an error page or a listing page?"},
    "price_present":  {"type": "noul", "instructions": "Is the `price` field filled in with a real price?"},
    "author_present": {"type": "noul", "instructions": "Is the `author` field filled in with a real author name?"},
    "currency_right": {"type": "noul", "instructions": "Is the `currency` field the right currency for the `price` shown on this listing?"},
    "category_right": {"type": "noul", "instructions": "Does the `category` field match what the title and description are actually about?"},
    "genre": {
        "type": "choice",
        "instructions": "Which genre does this book belong to?",
        "criteria": {g: None for g in ["poetry", "travel", "mystery", "science fiction",
                                       "history", "business", "self help", "art", "music", "humor"]},
    },
}
def ask_jev(state):
    response = requests.post(
        "https://api.typesafe.ai/v1/systemone",
        headers={"Authorization": f"Bearer {TYPESAFE_API_KEY}"},
        json={"state": state, "model": "jev-latest", "questions": QUESTIONS},
        timeout=120,
    )
    response.raise_for_status()
    return response.json()
Enter fullscreen mode Exit fullscreen mode

That is the whole integration: a dictionary in, six typed answers out. Basically a Jev powered data quality checker.

A good record, a bad one, and one that is quietly wrong

Jev responded :

{ "name": "A Light in the Attic", "author": null,
  "description": "It's hard to imagine a world without A Light i...",
  "price": "51.77", "currency": "GBP", "category": "Poetry",
  "availability": "In stock (22 available)" }
  is_a_book        0.85
  price_present    0.71
  author_present   0.01
  currency_right   0.61
  category_right   0.96
  genre            poetry (confidence 1.00)
Enter fullscreen mode Exit fullscreen mode

On a URL that does not exist:

{ "name": "404 Not Found", "author": null, "description": null,
  "price": null, "currency": null, "category": null, "availability": null }
  is_a_book        0.04
  category_right   0.08
  genre            humor (confidence 0.41)
Enter fullscreen mode Exit fullscreen mode

Jev rejects it, and author_present reports 0.01 on both, because this site publishes no author anywhere.
That second record should never have reached the model, though. The response was a 404, raise_for_status() would have ended it a line earlier, and a null check catches it anyway. My own filter says so. I show it because it is the failure people actually ship, not because it needs a model. This next one does.

The bug no check can see

Take the correctly scraped record and swap in the description of a different book.
Think of this as the zip bug, or the pagination bug, the off-by-one in a list comprehension, and I have shipped it more than once. Every field is populated, every type is right, the price is positive, and the status code is 200.

  name        : 'A Light in the Attic'
  description : 'WICKED above her hipbone, GIRL across her heart...'
  null check  : PASSES
  type check  : price float() = 51.77, positive
  category_right   0.04 <-- DATA QUALITY :D
  genre            mystery (confidence 0.69)
Enter fullscreen mode Exit fullscreen mode

category_right falls from 0.96 to 0.04, and the genre follows the planted description rather than the title, which is a tell that the model read the field instead of pattern-matching the name. Across 59 correct records and the same 59 with descriptions shifted by one, that question caught 46 crossings at a 0.5 threshold with zero false alarms, and 52 at 0.7 with one. Most of those crossings land within the same genre, which is the harder case.
Four checks in cost order: null, type, and range checks are free, and Jev is the last and narrowest layer
Now look again at the good record, because two of those answers are weaker than they should be. price_present came back 0.71 on a record where the price is there, and that is a question a null check answers perfectly and for free. currency_right came back 0.61, and that one is my fault: my parser strips the pound sign before building the record, so the state I sent Jev contained no evidence of any currency at all. It was right to shrug. The jaggedness page tells you to point a question at the relevant state, and I had deleted it. The strong answers, 0.96 and 1.00, are the judgment calls.

What this saves

I sent the identical record and the identical six questions to a cheap generative model, gpt-5.6-luna, routed through OpenRouter so its latency carries a hop that Jev's does not, three runs each:
| | latency | tokens | cost |
|---|---|---|---|
| Jev | 0.92s | 776 in, output free | $0.000033 |
| generative model | 3.8s to 8.9s | 457 in, 117 to 142 out | $0.000232 to $0.000262 |
Roughly 7X the cost, on one record. The latency spread is the more interesting half: Jev sat between 0.92 and 0.97 seconds across every run, while the generative model ranged from under 4 seconds to nearly 9. You can plan around the first number.
One deduction, in fairness: the shape advantage is smaller than it looks, because a generative API can be pushed into a schema with structured outputs. Cost and consistency are the real differences.
At a million records a day with six questions each, that gap is roughly $33 against $232, though TypeSafe's published ceiling of 1,200 requests a minute puts a million a day at about 58% of the limit before you ask for more. The marginal question is nearly free: a seventh check, or a twentieth, costs a few more input tokens for the wording and no extra time, because they are evaluated together.
That last point deserves a number against the alternative it replaces. On a separate run over 59 books, using the site's own category as the answer key and showing the model only the title and description, one Choice got 55 right against 43 for the best keyword list I could write. All four of Jev's misses were arguable rather than wrong: business against self help, twice, and travel against art. That run cost $0.00188 in total.

Your mileage will vary

Everything here ran against jev-1.13.0 on one teaching site, in English, on one afternoon, with three runs of the comparison. Treat it as a first look, not a benchmark.
Before relying on any of it, know this. Jev is not deterministic. Across repeated identical calls the confident answers held steady while the borderline ones drifted by several points, so do not put a threshold anywhere near where the answers wobble. I was ready to present that as my own finding until I read further into TypeSafe's cookbooks and found they had measured the same thing: six of eight Nouls came back with a standard deviation of exactly zero across five repeats while two carried noise, and their conclusion is the useful one. "The noise is a property of the question, not of how you batch."
A well-formed request can also encode the wrong question. I once passed a Choice's options as a list nested under a key called options, expecting an error. None came, because criteria accepts a map whose values may be arrays, so what I had sent was a valid one-option Choice. Jev returned options as the winner at a confidence of 1.0. Nothing was broken and the answer was useless: the type is guaranteed, the meaning is not.

Other ways to use it

None of these are tested. Triaging spider monitor alerts so a human only sees the ambiguous ones, which pairs naturally with Spidermon or a preflight check like scrapy-spidey-sense. Adjudicating deduplication candidates your own code has already shortlisted. Flagging listings whose description contradicts their own attributes.
One caution applies to all of them. The state you send is untrusted text off the internet, and TypeSafe is direct about it: "Content written to adversarially steer the model, whether that is an injected instruction, a deliberately misleading framing, or text that argues for its own classification, can move the answer." That last case should worry a scraper, because a page has every incentive to argue for its own classification. Their cookbook is blunter still: nothing here is a security boundary. The same thinking applies as to any model reading a hostile page.

Closing

Jev may never write you a selector. For now it is a cheap and very fast second opinion on data you already have, and the free output tokens mean you can afford that opinion on every record rather than on a sample.
I would ship it for the narrow job, with thresholds tuned on my own data and every cheap check running in front of it. I would not replace a null check with a model call.
As for the dog photo, it is the same shape of question I was writing classifiers for a decade ago. What changed is that I did not train anything. No labeled set, no feature engineering, and when I wanted an eleventh genre I added a key to a dictionary instead of collecting examples. That, and the price, is what changes which questions are worth asking at all.
Originally published on Zyte.

Top comments (0)