DEV Community

Jose Herrera
Jose Herrera

Posted on AI-assisted

How to use Jev's Choice API with a model that doesn't generate text

This post is about Jev's Choice API: how to send a question with a fixed set of answers and get back a probability for each one, and how I used it to build Ask Jev, an independent browser extension for Jev. Jev is TypeSafe's System One model, and it does not generate text. You give it a state and a set of choices, and it returns a probability distribution over those choices, which is what lets you ask a question about a webpage and get an answer instead of a paragraph. I am the developer of Ask Jev, and it is the extension I use as the running example here.

The problem: most questions I ask during a workday need an answer, not a conversation

I spend a good part of my day asking software yes or no questions. A vendor's pricing page says overage is billed monthly, and I need to know whether that is actually monthly or "on the anniversary." A pull request touches a file near the auth code, and I want to know whether it changes the auth path. A contract PDF has a clause that looks like it lets them change the price, and I want to know if it does.

None of those are conversations. I do not want four paragraphs I then have to read and summarize into the decision I already knew I was making. A chat model gives me prose and makes me extract the answer. That extra step is the whole problem. I wanted to press a key, ask the question, and get the answer.

That is the thing I built. Ask Jev is a Chrome extension. Press Command+J on any page, type a question, and instead of generated text you get a probability for each possible answer. It runs on TypeSafe's Jev model, which returns decisions rather than prose. The extension is mine, it is independent, and it is not affiliated with TypeSafe.

What Jev actually is

Jev is a model, served on TypeSafe's System One endpoint, that puts probability mass over a fixed set of answers instead of generating a reply. The part I use is the choice question type. You send it three things: a state (the text it should judge), a model string, and a questions object. For a choice question you give it instructions (your question as a string) and criteria (a map whose keys are the possible answers). It gives back a probability for each answer and which one it selected.

The request my extension builds looks like this:

{
  state: context,
  model,
  questions: {
    judgment: {
      type: "choice",
      instructions: question,
      criteria: { YES: null, NO: null, UNCLEAR: null },
    },
  },
}
Enter fullscreen mode Exit fullscreen mode

The criteria values are null on purpose. Only the keys matter. The keys are the labels you get probabilities for.

The smallest complete request

Here is a full request you can copy and run. It uses a plain string for state, which is the simplest valid form, and the endpoint, header, body, and model are the real ones.

curl https://api.typesafe.ai/v1/systemone \
  -H "Authorization: Bearer $TYPESAFE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "state": "Refunds are issued within 30 days of purchase.",
    "model": "jev-latest",
    "questions": {
      "judgment": {
        "type": "choice",
        "instructions": "Can I get a refund 45 days after purchase?",
        "criteria": { "YES": null, "NO": null, "UNCLEAR": null }
      }
    }
  }'
Enter fullscreen mode Exit fullscreen mode

An example response:

{
  "model": "jev-1.13.0",
  "answers": {
    "judgment": {
      "type": "choice",
      "choice": "NO",
      "probabilities": { "YES": 0.02, "NO": 0.96, "UNCLEAR": 0.02 },
      "confidence": 0.93
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The response also carries a usage block with input and output token counts. The probabilities above are an example, not a measured query. What matters for a builder is that there is no text field to render and no streaming to handle. There is choice, one string, the probabilities you asked for, one per label, and confidence, a single number derived from that distribution. My parser reads choice and probabilities and treats any distribution that does not sum to within 2 percent as an invalid response, showing an error rather than a wrong answer.

The same call in JavaScript:

const response = await fetch("https://api.typesafe.ai/v1/systemone", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.TYPESAFE_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    state: "Refunds are issued within 30 days of purchase.",
    model: "jev-latest",
    questions: {
      judgment: {
        type: "choice",
        instructions: "Can I get a refund 45 days after purchase?",
        criteria: { YES: null, NO: null, UNCLEAR: null },
      },
    },
  }),
});

const { answers } = await response.json();
console.log(answers.judgment.choice, answers.judgment.probabilities);
Enter fullscreen mode Exit fullscreen mode

How to get an API key

Create an account at console.typesafe.ai and generate a key on the API keys page, then export it as TYPESAFE_API_KEY so the official SDKs and the examples here can find it. TypeSafe briefly paused new signups in late September and reopened them on September 27, 2026, and as of this writing new accounts no longer receive the $5 free credit that early accounts got, so you pay list price from your first request. If you cannot get a TypeSafe account, Jev is also served through gateways like Vercel AI Gateway and OpenRouter, which issue their own keys under their own model names.

One naming detail that cost me an afternoon: on your own key the extension sends the model string from Settings, which defaults to jev-latest, while the free-tier proxy pins jev-1.13.0 so the free path does not move under users. The response echoes whichever model served the request, which is why the model field above reads jev-1.13.0 while the request asked for jev-latest.

I am not going to quote timings or accuracy figures. TypeSafe's terms restrict publishing performance information, I do not have accuracy numbers I would stand behind, and neither is useful for deciding whether the API fits your use case.

How the extension works

Press Command+J on macOS (Ctrl+Shift+K elsewhere) and a palette opens in the current tab inside a Shadow DOM, out of reach of page CSS. The toolbar icon does the same.

Context starts with your selection. With nothing selected, it walks main, article, [role='main'], falls back to body, drops tags that are never the answer (script, style, nav, header, footer, forms, inputs, canvas, dialog), skips hidden elements and duplicate lines, and truncates to a byte budget. The truncation binary-searches a UTF-8 boundary, so it never splits a character:

export function extractPageContext({ document, selection = ..., maxChars = DEFAULT_MAX_CONTEXT_CHARS }) {
  const selected = normalizeText(selection);
  if (selected) return truncateUtf8(selected.slice(0, maxChars).trimEnd(), maxChars);

  const root = document.querySelector("main, article, [role='main']") ?? document.body;
  if (!root) return "";

  return fitToLimit(collectText(root, document.title), maxChars);
}
Enter fullscreen mode Exit fullscreen mode

Drag a PDF in and pdfjs-dist parses it locally: the file is read from an ArrayBuffer, text is pulled page by page up to 500 pages, and the output is capped at 5 MB per file and 250 KB of text. The raw file never leaves the browser, which is what makes it reasonable to ask AI about a PDF you would not otherwise upload anywhere. TXT, Markdown, JSON, and CSV read the same way.

A PDF attached to the Ask Jev palette, shown as a chip above the possible answers

A dropped PDF appears as a chip in the palette. It is parsed in the browser; only its extracted text is sent with the question.

Everything the model sees shares one 24 KB budget. The state is { webpage, documents }, split across the page and each document with a binary search until the serialized state fits:

export function buildJevState(input, maxBytes = MAX_CONTEXT_BYTES): JevState {
  const allocations = allocateFairly(sourceTexts, available);
  const makeState = (scale: number): JevState => ({ /* page + docs truncated to alloc * scale */ });
  // 16-step binary search until serializedBytes(state) <= maxBytes
}
Enter fullscreen mode Exit fullscreen mode

Before anything is sent, the question and choices are validated: 500 characters max, two to eight answers, deduplicated case-insensitively, 80 characters each. Only the background worker holds the API key; the content script cannot read it.

Designing the UI for probabilities instead of prose

This is where I spent the most time, because there is no text to paste. The interface has to be the answer.

Each answer is one row. I take Jev's probability, multiply by 100, round it, draw a bar at that width, and highlight the row Jev selected:

const percent = Math.round(probability * 100);
const selected = label === result.judgment.selected;
// <span>{label}</span><strong>{percent}%</strong>
// <div className="ajev-track"><span style={{ width: `${percent}%` }} /></div>
Enter fullscreen mode Exit fullscreen mode

That selected field is mine, not the API's. The API returns the chosen label as choice; my parser maps it to a selected field internally, and result.judgment.selected reads that mapped value.

The Ask Jev result view, one row per answer with a percentage and a bar, the leading option highlighted

The result view. Every answer keeps its probability on screen; the leading option is highlighted, not substituted for the others.

Rounding is a real decision, and I chose to be wrong on purpose

I round each probability independently and never rescale the set to force the column to read 100 percent, so a three-way split can render as 33, 33, 33. Rescaling would mean changing Jev's numbers to tidy my own layout, and the number on screen would stop being the number the model returned.

I show the whole distribution, always

I use the chosen label only to highlight the leading row, never to hide the others. If the answer is 55/45, both numbers stay on screen with equal weight, because the reason to use this model is to see the closeness.

UNCLEAR is a default because a forced binary is worse

The default choices are YES, NO, and UNCLEAR. UNCLEAR exists because the model has to divide probability over whatever options you give it. With only YES and NO, a page that does not address my question still splits its mass between them, so a 60/40 can look like a lean when the page says nothing either way. UNCLEAR gives "this is not in here" somewhere to go.

That is a trade-off: UNCLEAR can act like a magnet and soak up mass that would otherwise separate the other options, softening a real lean. I keep it as a default anyway, because a false binary is a worse failure than a soft signal.

No confidence badge

I also left out a high, medium, or low confidence badge, even though the API returns a confidence value. Summarizing a probability with a coarser word is another layer of guessing, and it invites you to stop at the label instead of reading the number.

Enumerating the choices is the cost of admission

You write the possible answers before you ask, between two and eight. That is more setup than a chat box, and it is also what makes the output a decision instead of a paragraph. The trade-off is real: framing moves to you, and two bad options get probability spread across two bad options.

The free tier, and why v1 failed

The first version required you to bring a TypeSafe API key. That killed adoption, and it is not hard to see why. The value of this tool is one keystroke and an answer. Asking someone to find the pricing page, create an account, generate a key, and paste it in before their first answer puts the work before the payoff. For a tool whose whole value is that first answer, this was fatal: most people never got there.

So in v2 the key lives on a server I run. The free path is a Vercel serverless function (api/ask.ts and api/quota.ts) backed by Upstash Redis. It is not a generic open proxy, and that took more care than the feature itself.

Each free request is signed in the background worker with an HMAC over the timestamp and the exact body, using a secret compiled into that worker. The server checks that signature, checks the request's Origin against an allowlist of extension IDs, and then rebuilds the upstream request from the fields it expects instead of forwarding whatever arrived. Anything else is dropped.

I should be honest about what that does and does not buy. A secret shipped inside an extension can be extracted by anyone willing to open the package, so signing raises the cost of abuse rather than preventing it. The caps are the real protection: a forged request counts against the same per-install and global allowances as a real one, so there is a bounded amount of use to steal.

Every free request draws on a few shared caps: a short burst limit to stop hammering, a daily allowance per install, a daily allowance per network, and a daily allowance across everyone. The two that matter for cost are 100 questions per install per day and 10,000 questions per day in total. Counters live in Redis with day-long TTLs, and a request that gets turned away does not burn allowance it never used.

The global cap is the honest spend ceiling. Every request costs input tokens no matter who sends it, so a per-user cap on its own would not bound my bill, and I wanted the ceiling to be a number I could actually compute rather than guess at.

The math is short because Jev bills one thing. TypeSafe's published rate is $0.042 per million input tokens, and output is free. My state cap is 24 KB. English runs about 4.9 characters per token, so a state that spends the whole cap is roughly 4,900 tokens. Add the question, the choices, and a small fixed per-request overhead and a maxed request is about 5,200 tokens:

5,200 tokens × $0.042 / 1,000,000 = $0.00022
Enter fullscreen mode Exit fullscreen mode

So the most expensive request the proxy will ever forward costs about two hundredths of a cent. Applied to the caps:

  • Per install: 100 questions a day, every one maxed, is about $0.022 a day, or 66 cents a month.
  • Globally: 10,000 questions a day, every one maxed, is about $2.18 a day, or $65 a month.

Both are ceilings, not bills, and they assume every single request burns the entire 24 KB state budget, which almost none do. A question about one paragraph sends a fraction of that, which is a fraction of a fraction of a cent. The realistic total sits far below the ceiling. That is the whole reason the free tier can exist: the per-question cost is small enough to give away, and the caps stop the worst case from being open-ended.

When the global pool is spent, free questions pause for the rest of the UTC day and anyone can fall back to their own key. Settings shows how many are left.

The proxy is not published, so the free tier is the only shared path. If you run your own server on top of a TypeSafe key, check which TypeSafe agreement your key is under first. Some evaluation terms restrict sharing a credential or serving a product to third parties.

What I would do differently, and what Jev is not good for

I would ship the free path in v1. The API key requirement was the problem, not a feature I under-marketed. I spent time on a launch that mostly taught me the wall was in front of the door.

The keyboard shortcut was a smaller version of the same lesson. Command+J works on macOS, but the obvious alternatives collide with browser defaults (Command+Shift+J opens Downloads, Ctrl+Shift+J opens DevTools). I ended up with a platform split, a toolbar fallback, and a link to chrome://extensions/shortcuts. It works, and it is still the roughest edge in the extension.

I would also have decided the number display by watching people use it instead of reasoning about it. I am happy with showing every probability, but I reasoned my way there, and I would trust a week of watching over that.

What Jev is not for is as useful to know as what it is. It does not generate text, so it cannot summarize, draft, rewrite, or explain. It cannot answer a question with an open set of answers, because there is nothing to put probability mass on. It will not hand you a quote or a citation, because a distribution is not evidence. It is not a search engine. And it does not do multi-turn work; every question is a fresh state with no memory of the last one. It is a decision over an answer set you provide. If you can enumerate the answers, it is a good fit. If you cannot, it is the wrong tool and you will spend your time forcing it.

One more honest limit: it judges the text you give it. The context assembly above is doing as much work as the model is. Feed it the wrong paragraph and you get a confident distribution over the wrong paragraph.

Frequently asked questions

What is Jev's Choice API?

Jev's Choice API is the part of TypeSafe's System One model that takes a state and a set of answer options, then returns a probability for each option and the one it selected. It is built for questions that have a fixed set of answers rather than open-ended ones. In the extension, that is what turns a yes or no question about a page into a distribution you can read at a glance.

Can I use Jev without an API key?

Yes, but only through Ask Jev. The extension ships with a free daily allowance served through a small proxy I run, and that path needs no account and no key; you can also add your own TypeSafe key in Settings to remove the cap. Calling Jev's Choice API directly always requires a key, either a TypeSafe key or one from a gateway, so the keyless path is specific to my extension's free tier, not to Jev itself. That free path is shared and rate-limited, so the daily allowance is finite.

Does Jev generate text?

No. Jev returns typed decisions: a choice with probabilities, a score, or a yes or no probability. Giving up string generation is the point, and it is why the model cannot return text that contradicts its own answer. It is also why it cannot summarize, rewrite, or explain, which is the trade-off you accept when you use it.

Can I ask a question about a PDF?

Yes. Drag a PDF into the palette and you can ask AI about a PDF directly: the file is parsed locally in the browser with pdfjs-dist, and only the extracted text is sent with the question. You can also select text or ask a question about a webpage as it stands. The raw file never leaves your machine.

Do I need to write code to use Jev?

Not to use the extension. Install it, press Command+J, type a question, and edit the answer chips. You only write code if you are calling the Choice API yourself, and the smallest call is the curl example above. The API is plain HTTPS with a JSON body, so any language that can make a POST works.

What is Jev not good for?

Open-ended questions, summarization, drafting, and anything that needs citations or multiple turns. It also cannot invent the answer options for you, so if you cannot enumerate the possible answers, it will not help. And it only judges the text you give it, so a bad state produces a confident distribution over the wrong thing.

Where to find it

Ask Jev is a free browser extension for Jev, requires no account and no API key, and is on the Chrome Web Store. The extension is open source at github.com/JoseMiguelHerrera/ask-jev; the free-tier proxy is not published. The store listing is Ask Jev.

Top comments (0)