{"id":722,"date":"2026-09-24T08:01:51","date_gmt":"2026-09-24T08:01:51","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/typesafe-ai-jev-api-tutorial-choice-score-noul-and-the-gotchas\/"},"modified":"2026-09-24T08:01:51","modified_gmt":"2026-09-24T08:01:51","slug":"typesafe-ai-jev-api-tutorial-choice-score-noul-and-the-gotchas","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/typesafe-ai-jev-api-tutorial-choice-score-noul-and-the-gotchas\/","title":{"rendered":"TypeSafe AI Jev API Tutorial: Choice, Score, Noul and the Gotchas"},"content":{"rendered":"<p>Short version for the impatient: read the page called &ldquo;Jev 1.13 jaggedness&rdquo; before you read the quickstart. It&rsquo;s the most useful thing in the TypeSafe AI docs, and it will save you from writing the three bugs I was about to write.<\/p>\n<p>Some context. Earlier today I published a post about <a href=\"https:\/\/abrarqasim.com\/blog\/ml-vs-llm-for-classification-jev-decision-models-the-triage-bot-i-overbuilt\/\" rel=\"noopener\">overbuilding a triage bot with a chat model<\/a>, and in it I sketched what the Jev version might look like. I wrote that sketch from the launch post and Simon Willison&rsquo;s notes, and I said so at the time. Then I sat down with the actual docs and found my guessed field names were wrong, my idea of what &ldquo;confidence&rdquo; means was half wrong, and one of the patterns I&rsquo;d planned to use is specifically called out as unreliable.<\/p>\n<p>So this is the corrected version. Real request shape, real SDK calls, and the edges TypeSafe itself admits to. I&rsquo;m still waiting on an API key, so every response body below comes from TypeSafe&rsquo;s documentation, not from my terminal. I&rsquo;ll flag the places where that matters.<\/p>\n<h2 id=\"the-request-shape-in-one-curl-call\">The request shape in one curl call<\/h2>\n<p>Jev has one endpoint. You POST a <code>state<\/code> (the thing being judged) and a map of <code>questions<\/code>, and you get back one typed answer per question. The <a href=\"https:\/\/docs.typesafe.ai\/introduction\/quickstart\" rel=\"nofollow noopener\" target=\"_blank\">quickstart<\/a> shows this with a support ticket:<\/p>\n<pre><code class=\"language-bash\">curl -X POST https:\/\/api.typesafe.ai\/v1\/systemone \\\n  -H &quot;Authorization: Bearer $TYPESAFE_API_KEY&quot; \\\n  -H &quot;Content-Type: application\/json&quot; \\\n  -d '{\n    &quot;state&quot;: &quot;Hi, I have been trying to connect my Stripe account for 3 days and the integration keeps failing. I am losing sales. Please help ASAP.&quot;,\n    &quot;model&quot;: &quot;jev-latest&quot;,\n    &quot;questions&quot;: {\n      &quot;department&quot;: {\n        &quot;type&quot;: &quot;choice&quot;,\n        &quot;instructions&quot;: &quot;Which team should handle this&quot;,\n        &quot;criteria&quot;: {\n          &quot;billing&quot;: &quot;Payment or subscription issues&quot;,\n          &quot;technical&quot;: &quot;Bugs or integration problems&quot;,\n          &quot;sales&quot;: &quot;Pricing or account questions&quot;\n        }\n      },\n      &quot;is_urgent&quot;: {\n        &quot;type&quot;: &quot;noul&quot;,\n        &quot;instructions&quot;: &quot;The message conveys urgency or time-sensitivity&quot;\n      }\n    }\n  }'\n<\/code><\/pre>\n<p>And the documented response for the Choice question:<\/p>\n<pre><code class=\"language-json\">{\n  &quot;department&quot;: {\n    &quot;type&quot;: &quot;choice&quot;,\n    &quot;choice&quot;: &quot;technical&quot;,\n    &quot;confidence&quot;: 0.78,\n    &quot;probabilities&quot;: { &quot;technical&quot;: 0.85, &quot;sales&quot;: 0.0, &quot;billing&quot;: 0.15 }\n  }\n}\n<\/code><\/pre>\n<p>The thing I got wrong in my sketch: questions are a keyed object, not a list, and the key is yours. The docs are explicit that the key is never sent to the model. So <code>\"is_urgent\"<\/code> tells the model nothing. Whatever you want judged has to be in <code>instructions<\/code>. I had been naming my keys descriptively and assuming that helped. It doesn&rsquo;t.<\/p>\n<p>The other surprise is how little ceremony there is. No system prompt, no temperature, no max tokens. Pricing is per input token only ($0.042 per million), and output is free because there&rsquo;s no text being generated.<\/p>\n<h2 id=\"choice-score-and-noul-and-when-id-pick-each\">Choice, Score and Noul, and when I&rsquo;d pick each<\/h2>\n<p>There are three question types. The Python SDK (<code>pip install typesafe-sdk<\/code>, Python 3.10 or newer) gives each one a class:<\/p>\n<pre><code class=\"language-python\">from typesafe_sdk import Choice, Noul, Score, TypeSafeClient\n\nclient = TypeSafeClient()  # reads TYPESAFE_API_KEY\n\nresponse = client.system_one(\n    state={\n        &quot;ticket_message&quot;: &quot;My flight was cancelled. Can I get a refund?&quot;,\n        &quot;refund_policy&quot;: &quot;Cancelled flights are eligible for a full refund.&quot;,\n    },\n    questions={\n        &quot;refund_requested&quot;: Noul(\n            instructions=&quot;Does `ticket_message` request a refund?&quot;,\n        ),\n        &quot;request_type&quot;: Choice(\n            instructions=&quot;What is the main request in `ticket_message`?&quot;,\n            criteria={\n                &quot;refund&quot;: &quot;The customer wants money returned.&quot;,\n                &quot;rebooking&quot;: &quot;The customer wants a replacement flight.&quot;,\n                &quot;information&quot;: &quot;The customer is asking for information only.&quot;,\n            },\n        ),\n        &quot;frustration&quot;: Score(\n            instructions=&quot;How frustrated does the customer appear in `ticket_message`?&quot;,\n            criteria=[\n                &quot;Calm and neutral.&quot;,\n                &quot;Concerned but civil.&quot;,\n                &quot;Very angry or using strong language.&quot;,\n            ],\n        ),\n    },\n)\n\nprint(response.answers[&quot;refund_requested&quot;].noul)   # probability of yes\nprint(response.answers[&quot;request_type&quot;].choice)     # one of your keys\nprint(response.answers[&quot;frustration&quot;].score)       # a float along your levels\n<\/code><\/pre>\n<p>Notice the backticks around <code>ticket_message<\/code> in the instructions. That&rsquo;s the documented way to point a question at one field of a structured state, and it&rsquo;s a nice touch. You can hand Jev a whole ticket, order and policy object and aim each question at the part it should care about.<\/p>\n<p>My rule of thumb after reading the <a href=\"https:\/\/docs.typesafe.ai\/primitives\" rel=\"nofollow noopener\" target=\"_blank\">primitives page<\/a>:<\/p>\n<p>Noul when the question is a clean yes or no and the probability itself is useful, like &ldquo;does this message contain an email address&rdquo;. Choice when there&rsquo;s a fixed set of unordered options and each one maps to a code path. Score when the answer sits on a spectrum you can describe level by level.<\/p>\n<p>The trap is using Noul for a spectrum. The docs give the example &ldquo;Is this candidate strong in Python?&rdquo;, and point out that a Noul of 0.5 means the model thinks yes and no are equally likely. It does not mean &ldquo;medium skill&rdquo;. If you want medium skill, write a Score with levels. I would absolutely have made this mistake, because 0.5 looks like &ldquo;halfway&rdquo; when you&rsquo;re skimming logs at 11pm.<\/p>\n<h2 id=\"confidence-is-math-on-the-probabilities\">Confidence is math on the probabilities<\/h2>\n<p>I assumed <code>confidence<\/code> was a separate signal the model produced. It isn&rsquo;t. The <a href=\"https:\/\/docs.typesafe.ai\/confidence\" rel=\"nofollow noopener\" target=\"_blank\">confidence page<\/a> says it&rsquo;s a statistic computed from how peaked the probability distribution is. All the mass on one option gives 1.0, an even spread gives 0.<\/p>\n<p>Their interactive demo uses <code>(n * largest - 1) \/ (n - 1)<\/code> as an approximation for n options. I checked it against the quickstart response. Three options, largest probability 0.85: (3 x 0.85 &#8211; 1) \/ 2 = 0.775. The documented response says 0.78. So the approximation holds, at least on their example.<\/p>\n<p>Two practical consequences.<\/p>\n<p>First, Noul answers have no <code>confidence<\/code> field at all. A Noul is already a single probability, so your threshold goes directly on <code>noul<\/code>. I had a helper function that read <code>.confidence<\/code> off every answer. It would have thrown on the first Noul.<\/p>\n<p>Second, because you get the full <code>probabilities<\/code> dict, you can compute your own measure. The docs say this outright. For a routing decision I care more about the gap between the top two options than about overall flatness, so I&rsquo;ll probably compute <code>top1 - top2<\/code> myself and threshold on that. The confidence page also shows a nice pattern where the threshold depends on the action: a read-only action runs above 0.5, while an irreversible one needs 0.9 plus a confirmation step. That&rsquo;s the part I&rsquo;d copy into any real integration.<\/p>\n<h2 id=\"the-jaggedness-page-is-the-part-to-read-twice\">The jaggedness page is the part to read twice<\/h2>\n<p>TypeSafe publishes a page listing where <a href=\"https:\/\/docs.typesafe.ai\/model-jaggedness\/jev-1.13\" rel=\"nofollow noopener\" target=\"_blank\">jev-1.13 is known to fail<\/a>. Vendors rarely do this, and I&rsquo;ll give them real credit for it. Here are the entries that changed my plans.<\/p>\n<p>Counting. Jev does not count reliably, and the error grows with the size of the thing being counted. The recommended fix is to loop in code, ask one Noul per item, and sum the answers yourself. They show this with a list of words and a &ldquo;is this a fruit&rdquo; Noul per index.<\/p>\n<p>Dates. Jev reads dates as text, so &ldquo;which date is earlier&rdquo; or &ldquo;is this inside the settlement window&rdquo; is unreliable. Their fix is to extract each part (day, month, year) as a Choice over a closed set, with a &ldquo;not stated&rdquo; option, then do the date arithmetic in code. This one stung, because half of my planned invoice checks were date comparisons.<\/p>\n<p>Structural invariants. This is the one I found most interesting. The same refund question asked as a Noul returned 0.22, while asked as a yes\/no Choice it returned 0.01 for yes. And on a different ticket, &ldquo;is this a refund&rdquo; plus &ldquo;is this something other than a refund&rdquo; summed to 1.19. So you can&rsquo;t tune a threshold on a Noul and carry it over to a Choice, and you can&rsquo;t assume a question and its negation are complementary. Pick one phrasing per decision and stick with it.<\/p>\n<p>Adversarial content. State is treated as data, and text that argues for its own classification can move the answer. That matters a lot if you plan to use Jev as a filter on user input, which is exactly what I plan to do. More on that in a separate post.<\/p>\n<p>Literal reading. Jev answers the words you wrote, not the intent behind them. Their advice is good general advice: when you catch yourself explaining what you really meant, that explanation is the missing half of the instruction.<\/p>\n<h2 id=\"pin-the-model-version-before-you-tune-anything\">Pin the model version before you tune anything<\/h2>\n<p>The quickstart uses <code>jev-latest<\/code>. The <a href=\"https:\/\/docs.typesafe.ai\/models\" rel=\"nofollow noopener\" target=\"_blank\">models page<\/a> explains that aliases move when a release ships, so your answers can change without a deploy on your side. If you&rsquo;ve tuned confidence thresholds against a specific version, pin the versioned ID (<code>jev-1.13.0<\/code> right now) and upgrade on your own schedule. Every response includes the <code>model<\/code> field that actually answered, so log it next to each decision.<\/p>\n<p>A few other limits from that page I&rsquo;d write on a sticky note. Context is 64k tokens per request, with 32k for the state plus the longest single question. Input is text only, so images need to become text or structured fields first. English is the primary training language, and other languages work less well. Rate limits are listed at 250,000 tokens per second and 1,200 requests per minute, with a warning that they&rsquo;re adjusting while demand is high.<\/p>\n<p>And one thing I liked: there&rsquo;s no fine-tuning. The same weights serve everyone, and you customise through state, instructions and criteria. That&rsquo;s less flexible than training your own classifier, but it also means there&rsquo;s no training pipeline to maintain. For the kind of <a href=\"https:\/\/abrarqasim.com\" rel=\"noopener\">client projects I usually take on<\/a>, that trade is usually worth it.<\/p>\n<h2 id=\"where-this-fits-next-to-structured-output\">Where this fits next to structured output<\/h2>\n<p>If you&rsquo;ve been getting typed answers out of chat models with JSON schemas, you already know the failure modes. I wrote about a small model trained specifically for <a href=\"https:\/\/abrarqasim.com\/blog\/llm-structured-output-the-350m-model-and-the-three-rewards-that-matter\/\" rel=\"noopener\">structured output<\/a> a while back, and the lesson there was that valid JSON and a correct answer are different problems. Jev solves the first one by construction, since it can&rsquo;t return an option you didn&rsquo;t define. It doesn&rsquo;t solve the second. A wrong answer with a correct type is still a wrong answer, and the confidence number is the only warning you get.<\/p>\n<p>So I&rsquo;d treat Jev as a replacement for the classification step, not for the whole pipeline. Anything that needs generated text still goes to a chat model. Anything that needs arithmetic stays in code.<\/p>\n<h2 id=\"what-to-do-with-this-before-you-get-a-key\">What to do with this before you get a key<\/h2>\n<p>The playground at console.typesafe.ai works once you&rsquo;re off the waitlist, but you can prepare now. Pick one decision your app makes today with a chat model or a pile of regexes. Rewrite it as one to five questions, using the rules above: one judgment per question, the full question text in <code>instructions<\/code>, a Score wherever you were tempted to use a Noul for &ldquo;how much&rdquo;, and no dates or counts left for the model. Then pull 50 real examples from your logs with the answer a human gave. When the key arrives you&rsquo;ll have a test set on day one, and you can see whether the confidence numbers mean anything on your data instead of on theirs.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>A hands-on TypeSafe AI tutorial for Jev: the real request shape, Choice vs Score vs Noul, how confidence is computed, and the failure modes the docs admit to.<\/p>\n","protected":false},"author":2,"featured_media":721,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"A hands-on TypeSafe AI tutorial for Jev: the real request shape, Choice vs Score vs Noul, how confidence is computed, and the failure modes the docs admit to.","rank_math_focus_keyword":"typesafe ai","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[4],"tags":[811,639,807,530,810],"class_list":["post-722","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ai","tag-api-tutorial","tag-classification","tag-jev","tag-python","tag-typesafe-ai"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/722","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/comments?post=722"}],"version-history":[{"count":0,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/722\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/721"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=722"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=722"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=722"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}