Skip to Content

typia.llm.evaluation — typed questions for evaluation models

An evaluation model generates no text. You give it a shared state and a map of typed questions, and it answers every question with probabilities: a yes/no probability, one option of a closed set, or a position on ordered levels. TypeSafe’s Jev  is the native one, and Vercel AI SDK’s experimental_evaluate() runs the same provider-neutral question shape on OpenAI, Anthropic, and Google as well.

typia.llm.evaluation<T>() turns a TypeScript decision type into those questions, and folds the answers back into T.

signature
export namespace llm { function evaluation<T extends Record<string, any>>(): ILlmEvaluation<T>; }
what-you-get
interface ILlmEvaluation<T> { questions: Record<string, ILlmEvaluation.IQuestion>; // hand this to the model decode: ( answers: unknown, rounding?: ILlmEvaluation.IRounding, ) => IValidation<T>; // answers in, T out }

This feature is experimental. It follows Vercel AI SDK’s evaluation model specification, which is itself experimental and may change in patch releases.

First example

triage.ts
import { experimental_evaluate } from "ai"; // AI SDK >= 7.0.103 import typia from "typia"; enum Department { /** * Payments, invoicing, refunds. * @probability 0.8 */ billing = "billing", /** * Bugs, outages, integrations. * @probability 0.5 */ technical = "technical", /** * Pricing, upgrades, new accounts. * @probability 0.5 */ sales = "sales", } enum Frustration { /** Calm or neutral */ calm = 0, /** Annoyed but cooperative */ annoyed = 1, /** Angry or threatening to leave */ angry = 2, } interface ITicketTriage { /** Does the customer convey urgency? */ urgent: boolean; /** Which team should handle this ticket? */ department: Department; /** How frustrated is the customer? */ frustration: Frustration; /** Which products does the customer mention? */ products: Array<"card" | "loan" | "deposit">; refund: { /** * Does the customer ask for a refund? * @probability 0.8 */ requested: boolean; }; } const ticket = "I was charged twice. Refund it now, or I leave."; const triage = typia.llm.evaluation<ITicketTriage>(); const result = await experimental_evaluate({ model: "typesafe-ai/jev", state: ticket, questions: triage.questions, }); const decoded = triage.decode(result.answers, result.rounding); if (decoded.success) decoded.data; // ITicketTriage // raw probabilities stay in the answer map, keyed by readable paths result.answers["refund.requested"]; // { type: "boolean", probability: 0.83 }

undefined

typia
export namespace llm { export function evaluation<T extends Record<string, any>>(): ILlmEvaluation<T>; }

The transform replaces the call with one compile-time plan. Calling TypeSafe’s API directly looks like this:

examples/src/llm/evaluation.ts
import { TypeSafeClient } from "@typesafe-ai/sdk"; import { toJevQuestions } from "@typia/jev"; import typia, { tags } from "typia"; enum Department { /** * Payments, invoicing, refunds * * @probability 0.5 */ billing = "billing", /** * Bugs, outages, integrations * * @probability 0.75 */ technical = "technical", /** * Pricing, upgrades, new accounts * * @probability 0.5 */ sales = "sales", } enum Frustration { /** Calm or neutral */ calm = 0, /** Annoyed but cooperative */ annoyed = 1, /** Angry or threatening to leave */ angry = 2, } interface ITicketTriage { /** Does the customer convey urgency? */ urgent: boolean; /** Which team should handle this ticket? */ department: Department; /** How frustrated is the customer? */ frustration: Frustration; /** Which products does the customer mention? */ products: Array<"card" | "loan" | "deposit">; refund: { /** Does the customer ask for a refund? */ requested: boolean & tags.Probability<0.8>; }; } const main = async (): Promise<void> => { // Generate the questions and checked answer decoder. const triage = typia.llm.evaluation<ITicketTriage>(); // Ask TypeSafe's Jev in its own wire format const client = new TypeSafeClient(); // reads TYPESAFE_API_KEY const { answers } = await client.systemOne({ model: "jev-1.13.0", // pinned: thresholds are tuned per model version state: "I was charged twice this morning. Refund it now, or I leave.", questions: toJevQuestions(triage.questions), }); // Check and decode the answers into ITicketTriage. // The direct TypeSafe SDK does not declare rounding precision. const result = triage.decode(answers); if (result.success === false) { console.error("Evaluation failed:", result.errors); return; } console.log("Triage:", result.data); console.log("Department probabilities:", answers.department); }; main().catch(console.error);

When to use this

If you need…Use
Decisions over closed sets, with probabilities, from an evaluation modelevaluation<T>()
Generated data of any JSON shape, from a language modelstructuredOutput<T>()
Function calling, where the LLM picks functionsapplication<Class>()

Type mapping

Every leaf property of T becomes one question. Its JSDoc description is the question text, because the question key is not sent to the model.

Property typeQuestionValue in T
booleanbooleantrue when P(true) reaches the threshold, 0.5 by default
string enum, or string literal unionchoicethe returned option
numeric enum, or numeric literal unionscore, levels in ascending value orderthe level (see below)
Array<U> of a string literal union or string enumone boolean per memberthe members decided true
nested objectflattened, one question per leafthe object rebuilt

Anything an evaluation model cannot answer is a compile error that names the property: string, number, and other open types; a single literal; a union mixing question kinds; optional, nullable, @hidden, or @internal properties; arrays of anything but a string literal union or string enum, and array type tags; tuples, dynamic keys, Map, Set, and recursive types; a leaf without a JSDoc description; and validation refinements such as tags.MinLength that the answer decoder cannot enforce. Documentation-only tags may remain.

Public class and interface getters or setters are decision properties, with their JSDoc supplying the question and optional @probability. Methods, private or protected members, symbol-keyed members, and call or construct signatures cannot be decoded into T and are compile errors.

criteria tells the model how to distinguish the available answers. For a choice it maps each string value to its enum member’s JSDoc description. For a score it is an array of numeric enum member descriptions, sorted by numeric value. The provider’s score is a possibly fractional position on the array’s index scale, from 0 to the last index. decode() selects a level by the rules below and maps it back to the enum’s numeric value. A missing choice description becomes null, while a missing score description becomes the numeric value written as text.

For the Frustration enum above, triage.questions.frustration is:

{ type: "score", instructions: "How frustrated is the customer?", criteria: ["Calm or neutral", "Annoyed but cooperative", "Angry or threatening to leave"], }

A set member’s question is the property’s description followed by Does the option "card" apply? and the member’s description, if any. That sentence is written by typia in English, so keep it in mind when the rest of your JSDoc is in another language.

A nested object’s own JSDoc is not sent anywhere; only the leaves’ descriptions become questions.

Question keys are the property paths in typia’s accessor notation, such as refund.requested, products.card, or ["with space"].

Decode answers

decode() checks the provider’s answer map and converts it into T, returning IValidation<T>. It is not typia.validate<T>(): that function checks an already-formed T, while decode() reads the different answer-map shape. For a supported decision type, a successful decode needs no second general validation. It accepts both the neutral boolean answer { type: "boolean", probability } and TypeSafe’s native { type: "noul", noul }, and ignores TypeSafe’s extra confidence and legend fields.

  • Boolean: true when P(true) reaches the threshold.
  • Choice: the returned option.
  • Score: the most probable level when the answer has probabilities, where a tie picks the lower level; otherwise the level nearest to the fractional score, where a half rounds up. The distribution wins because a bimodal answer’s rounded mean can be its least likely level.
  • Set: the members whose P(true) reaches their threshold.

A missing or extra answer, a wrong answer type, an undeclared option, or a probability or score out of range fails with typia’s usual error paths, such as $input.refund.requested. A choice or score may omit probabilities; when it includes them, the map must contain every option exactly once and sum to 1 within the provider’s declared rounding precision. The selected choice must have maximum probability, and a score must agree with the distribution’s weighted mean within that precision, following AI SDK’s evaluation answer contract. Pass result.rounding as the second argument when using experimental_evaluate(). Without a declaration, decode() uses the SDK’s strict 1e-6 tolerance; it does not guess a provider’s precision.

Probability requirements

tags.Probability<N> and its JSDoc spelling @probability N attach a probability requirement to a decision. They carry metadata only: is(), validate(), and JSON schemas ignore them.

SpellingMeaning
boolean & tags.Probability<N>boolean threshold: true only when P(true) ≥ N
"a" & tags.Probability<N> in a unionthat option’s acceptance minimum; in an array set, its inclusion threshold
@probability N on every enum membereach member’s acceptance minimum; in an enum array set, its inclusion threshold
@probability N on a propertythe boolean threshold, or the default minimum (for a set, the default threshold) of members without an override

A member’s own requirement wins over the property’s. Probability requirements are all-or-none for a choice, score, or set: once one member declares one, every member must declare one or the property must provide the default. The compiler rejects a partially covered type. If no member and no property declares a requirement, a choice or score has no minimum and every set member uses the 0.5 threshold.

Put @probability on the decision property or enum member, not on a type, interface, class, or enum declaration. When the program uses typia.llm.evaluation(), the compiler rejects declaration-level tags even on unused declarations: TypeScript erases the identity of primitive aliases, so use-site detection cannot reliably distinguish an unused alias from a used one.

@probability belongs to the property where it is written. An indexed-access type such as Source["threshold"] extracts the property’s type, not its JSDoc; put another @probability on the resulting evaluation property if it needs the same requirement. Passing Source itself to evaluation<Source>() does read the original property’s comment. For a requirement that must travel through indexed access or generic aliases, put tags.Probability<N> on the boolean or individual literal member type; type tags survive that extraction, including members of an array set.

gated.ts
enum Action { /** * Page the on-call. * * @probability 0.8 */ escalate = "escalate", /** * Answer the customer. * * @probability 0.5 */ reply = "reply", } interface IDecision { /** What should happen next? */ action: Action; }

When the selected option has a minimum and its probability is below it, decode() fails for that path. It never falls back to a less likely option, because that would invert the model’s judgment. A required option also fails when the answer has no probabilities to prove it, which is always the case on the OpenAI, Anthropic, and Google adapters. If the type declares no probability requirement at all, an answer without probabilities passes.

A member requirement lives on the enum, so it applies at every property that uses that enum.

Thresholds are tuned against one model’s calibration. TypeSafe’s aliases such as jev-latest move when a new release ships, so pin the versioned model ID when you tune thresholds.

Calling Jev directly

Jev’s own wire format, shared by TypeSafe’s API and OpenRouter’s Decisions API, spells the boolean question type "noul". @typia/jev converts the questions; the answers need no conversion.

typesafe.ts
import { TypeSafeClient } from "@typesafe-ai/sdk"; import { toJevQuestions } from "@typia/jev"; import typia from "typia"; enum Urgency { /** Can wait */ low = 0, /** Respond today */ medium = 1, /** Respond immediately */ high = 2, } interface ITicketTriage { /** Does the customer need an urgent response? */ urgent: boolean; /** How soon should the team respond? */ urgency: Urgency; } const ticket = "The payment failed and today's deadline is approaching."; const triage = typia.llm.evaluation<ITicketTriage>(); const client = new TypeSafeClient(); const { answers } = await client.systemOne({ state: ticket, questions: toJevQuestions(triage.questions), }); // The direct TypeSafe SDK does not return `rounding` metadata. const decoded = triage.decode(answers); if (decoded.success) decoded.data.urgency; // Urgency member answers.urgency; // { type: "score", score: 1.6, probabilities: { "0": 0, "1": 0.4, "2": 0.6 }, ... }

The direct call checks distribution sums and score consistency strictly. Pass an explicit rounding precision to decode() only when your endpoint guarantees it; unlike AI SDK’s experimental_evaluate(), this SDK does not return a precision declaration.

Providers

  • Calibration: probabilities are calibrated only on native evaluation models such as Jev. The AI SDK adapters for OpenAI, Anthropic, and Google ask a language model to write each number itself in one structured-output request, and return no choice or score distribution.
  • Independence: Jev evaluates each question independently, and the AI SDK adapters for language models instruct the model to do the same, so refund.requested at 0.1 next to a confident refund reason is a valid result. Ask dependent questions in a second request.
  • Limits: TypeSafe accepts at most 255 choice options and 10 score levels; other providers have their own limits. typia checks only the neutral constraints at compile time.
  • Language: Jev documents lower accuracy for non-English state and instructions, which includes non-English JSDoc.
Last updated on