Tasks / Classification

Using Jev as a classifier.

Jev, TypeSafe's System One model, can classify text against labels you describe in plain language in each request, with no task-specific training step. The design decision that matters most is whether more than one label can be true at once. If exactly one label wins, ask one Choice. If labels can co-occur, ask one Noul per label.

Both return probabilities rather than a bare label. What happens when those probabilities are not decisive is up to your code: route to a person, fall back to a broader category, or hand the case to another model.

Pick the classification shape before writing labels

Your problemExampleAsk JevRead
Exactly one label appliesOne queue owns each ticketOne choice() with every label, plus other.choice, .confidence, .probabilities
Any number of labels can applyA ticket asks for a refund and threatens to cancelOne noul() per label.noul on each, with its own cutoff
Several independent dimensionsQueue, plus risk flagsSeveral keyed questions in one systemOne() callEach answer under its own key
Labels are ordered levelsSeverity from low to criticalscore() (see Score).score, which can fall between levels
The answer is not decisiveA vague one-line messageNothing extraYour own rule on confidence or P(yes)
!The common mistake is using Choice for labels that can co-occur. Choice probabilities always sum to 1. When two labels are both true, they split the probability, and you get back one label with low confidence. That looks like ambiguity, but the input was clear.

Single-label: one Choice, one winner

Criteria are an object: each key is a label your code will receive, each value describes when that label applies. The TypeScript SDK turns the keys into a union type, so a misspelled label in a switch is a compile error.

classify-message.ts
import { choice, TypeSafeClient } from "@typesafe-ai/sdk";

const client = new TypeSafeClient(); // reads TYPESAFE_API_KEY

const { answers } = await client.systemOne({
  state: { message: "SSO login keeps looping back to the sign-in page." },
  questions: {
    queue: choice("Which queue should own `message`?", {
      billing: "Charges, invoices, refunds, plan changes, or payment methods",
      technical: "Errors, outages, bugs, or integration problems",
      account: "Sign-in, SSO, seats, permissions, or account settings",
      other: "None of the queues above fits",
    }),
  },
});

const queue = answers.queue.choice;
//    ^? "billing" | "technical" | "account" | "other"

// probabilities cover every label and sum to 1.
const [first, second] = Object.values(answers.queue.probabilities).sort(
  (a, b) => b - a,
);
const margin = first - second;
01Give the model a way out. TypeSafe recommends an other or “none of the above” label when your list may not cover every input. Without it, an off-topic message still gets assigned to one of your real queues.
02Confidence is not the winning probability. TypeSafe computes confidence from the shape of the whole distribution: 1.0 when all probability sits on one label, lower as it spreads. The probabilities map is still there if another measure suits you better, such as the margin between the top two labels.
03Up to 255 labels per Choice. Past that, or with a deep taxonomy, classify one level at a time. TypeSafe's hierarchical cookbook asks one Choice per level and keeps a few candidate paths alive rather than committing to the first branch.

Multi-label: one Noul per label

A Noul answers one yes/no question and returns a single number, the probability of yes. Ask one per label and each is judged separately, so a ticket can be a refund request and a cancellation threat at the same time. There is no confidence field on a Noul. The probability already expresses the uncertainty.

Same ticket, a refund request that also says the customer will cancel, asked both ways. Values are illustrative, not measured output.

Wrong shape: Choice
// Labels share one probability mass
"flag": {
  "type": "choice",
  "choice": "refund_requested",
  "confidence": 0.32,
  "probabilities": {
    "refund_requested": 0.49,
    "cancellation_intent": 0.47,
    "security_concern": 0.02,
    "none": 0.02
  }
}
Right shape: one Noul per label
// Each label gets its own P(yes)
"refund_requested": {
  "type": "noul", "noul": 0.93
},
"cancellation_intent": {
  "type": "noul", "noul": 0.88
},
"security_concern": {
  "type": "noul", "noul": 0.04
}

Noul values for different labels are not constrained to relate to each other. Choose each label's cutoff by what a mistake on that label costs. TypeSafe also warns against reusing a cutoff tuned on a Noul for a Choice.

Classify several dimensions in one request

Real classifiers rarely answer one question. This support-triage function asks for one owning queue and three independent flags against the same ticket in a single call. The questions are evaluated in parallel, and the ticket is sent once instead of once per question.

classify-ticket.ts
import { choice, noul, TypeSafeClient } from "@typesafe-ai/sdk";

const client = new TypeSafeClient();

// A type alias, so it is assignable to the SDK's JsonValue (see below).
type Ticket = {
  subject: string;
  body: string;
  plan: "free" | "pro" | "business";
};

type Flag = "refund_requested" | "security_concern" | "cancellation_intent";

// Illustrative cutoffs to show control flow. Replace them with values
// chosen on your own labeled tickets and the cost of each mistake.
const QUEUE_MIN_CONFIDENCE = 0.8;
const FLAG_CUTOFF: Record<Flag, number> = {
  refund_requested: 0.7,
  security_concern: 0.3, // a missed report costs more than a false alarm
  cancellation_intent: 0.6,
};

export async function classifyTicket(ticket: Ticket) {
  const { answers, model } = await client.systemOne({
    state: { ticket },
    questions: {
      // Single-label: exactly one queue owns the ticket.
      queue: choice("Which support queue should own `ticket`?", {
        billing: "Charges, invoices, refunds, plan changes, or payment methods",
        technical: "Errors, outages, bugs, or integration problems",
        account: "Sign-in, SSO, seats, permissions, or account settings",
        other: "None of the queues above fits",
      }),
      // Multi-label: each flag is judged on its own and may co-occur.
      refund_requested: noul(
        "Does `ticket.body` explicitly ask for money back?",
      ),
      security_concern: noul(
        "Does `ticket` report a compromised account or leaked credentials?",
      ),
      cancellation_intent: noul(
        "Does `ticket` say the customer plans to cancel or not renew?",
      ),
    },
  });

  const flags = (Object.keys(FLAG_CUTOFF) as Flag[]).filter(
    (flag) => answers[flag].noul >= FLAG_CUTOFF[flag],
  );

  const reasons: string[] = [];
  if (answers.queue.choice === "other") reasons.push("no queue fits");
  if (answers.queue.confidence < QUEUE_MIN_CONFIDENCE) {
    reasons.push("queue uncertain");
  }
  if (flags.includes("security_concern")) reasons.push("security flag");

  return {
    queue: answers.queue.choice,
    flags,
    route: reasons.length ? ("human_review" as const) : ("auto" as const),
    reasons,
    // Keep the raw numbers: you need them to tune the cutoffs later.
    evidence: {
      model,
      queueProbabilities: answers.queue.probabilities,
      refund: answers.refund_requested.noul,
      security: answers.security_concern.noul,
      cancellation: answers.cancellation_intent.noul,
    },
  };
}
Point at the field that matters. TypeSafe suggests nested JSON state with backtick paths such as `ticket.body` in the question. Only send fields a question needs; unrelated context lowers accuracy.
State must type-check as JSON. Every value inside state must be assignable to the SDK's JsonValue type. A type alias of JSON fields passes. An interface fails unless it declares [key: string]: JsonValue, because TypeScript gives only type aliases an implicit index signature. A Date field fails too; send an ISO string. We hit the interface case compiling the example.

Both TypeScript examples type-check under strict against @typesafe-ai/sdk 0.6.0. We confirmed the request body the SDK sends with a stubbed fetch; we did not run them against the live API, and no answers on this page come from a real request.

Decide in code what counts as uncertain

Jev always returns an answer. It does not abstain. Holding back is your application's job, and the signals are all in the response:

SignalWhat it meansTypical action
Low Choice confidenceProbability is spread across labelsHuman review, or report the parent category instead
Small top-two marginTwo specific labels are competingCheck whether the labels overlap; if both can be true, switch to Nouls
other winsInput is outside your label setTriage queue; review these to find missing labels
Noul near the middleYes and no are closeReview, or a slower model that can reason and explain
No universal threshold. The cutoffs in the example only show the control flow. Set real ones by running Jev on items you have already labeled, then plotting what share each cutoff automates and how many of those it gets wrong. Different actions deserve different cutoffs. A wrong queue is cheap; an ignored security report is not. Calibration describes groups of predictions, not any single answer.

For a Choice-driven dispatcher that sends requests to code, an LLM, or a person, see model routing with Jev.

When another classifier is the better tool

Jev is not the default answer to every labeling problem. These are our editorial rules of thumb, not measured comparisons.

ApproachBetter whenJev instead when
Rules, regex, SQLThe signal is literal: a field value, a domain, an amount, a date window. Anything involving counting, dates, or arithmetic, which TypeSafe lists as jev-1.13 weaknesses.The rule would need to understand wording, and keyword lists keep growing exceptions
Embeddings and nearest neighborThousands of labels, labels defined by examples rather than descriptions, or deduplication and searchYou want a decision with probabilities among a short list of described labels. Embeddings can shortlist candidates for a Choice.
Trained classifierYou have plenty of labeled data, the label set is stable, and you need to run it on your own hardware or tune it to your dataYou have no training set yet, or labels change often enough that retraining is the bottleneck
Generative LLMYou need an explanation, extracted text, an open-ended label, or multi-step reasoning, which TypeSafe says lowers jev-1.13 accuracyThe answer is a known label and you want typed output with probabilities instead of parsed text (see Jev vs. LLMs)

Measure before committing. Published classification results are collected, with methodology notes, on the benchmarks page; none of them replace a test on your own data.

Limits that shape label design

01Size: up to 255 labels per Choice. 64k tokens per request, with 32k for state plus the longest question. Text and JSON only; no images or audio.
02Classified text can steer the answer. TypeSafe notes that injected instructions or text arguing for its own label can move the result. Write explicit criteria and test with hostile samples before classifying user content at scale.
03Wording is literal. Negations and scope words are read at face value, and rephrasing the same question can change the answer. Keep one question per judgment and version your criteria text like code.
04Numbers belong in code. Compute counts, date differences, and thresholds before the call and pass the result or a named bucket into state.
05Pin for stable labels. jev-latest can move to a new model. Log model from each response, and pin jev-1.13.0 while you re-check cutoffs on a new release.

Client setup, errors, and retries are covered in the TypeScript SDK guide; keys and rate limits in API access.

Not affiliated with, endorsed by, or operated by TypeSafe AI. Vendor claims are cited and attributed.