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 validate: (answers: unknown) => 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, { tags } from "typia"; enum Department { /** Payments, invoicing, refunds */ billing = "billing", /** Bugs, outages, integrations */ technical = "technical", /** Pricing, upgrades, new accounts */ sales = "sales", } interface ITicketTriage { /** Does the customer convey urgency? */ urgent: boolean; /** Which team should handle this ticket? */ department: Department; /** How frustrated is the customer? */ frustration: 0 | 1 | 2; /** 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 triage = typia.llm.evaluation<ITicketTriage>(); const result = await experimental_evaluate({ model: "typesafe-ai/jev-latest", state: ticket, questions: triage.questions, }); const validation = triage.validate(result.answers); if (validation.success) validation.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 { LlmEvaluation } from "@typia/utils"; import typia, { tags } from "typia"; enum Department { /** Payments, invoicing, refunds */ billing = "billing", /** * Bugs, outages, integrations * * @probability 0.75 */ technical = "technical", /** Pricing, upgrades, new accounts */ sales = "sales", } interface ITicketTriage { /** Does the customer convey urgency? */ urgent: boolean; /** Which team should handle this ticket? */ department: Department; /** How frustrated is the customer? */ frustration: 0 | 1 | 2; /** 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 the converting validator const triage = typia.llm.evaluation<ITicketTriage>(); // Ask TypeSafe's Jev directly, in its native wire format const response: Response = await fetch( "https://api.typesafe.ai/v1/systemone", { method: "POST", headers: { Authorization: "Bearer <YOUR_TYPESAFE_API_KEY>", "Content-Type": "application/json", }, body: JSON.stringify({ 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: LlmEvaluation.toTypeSafe(triage.questions), }), }, ); const { answers } = (await response.json()) as { answers: unknown }; // Validate the answers and fold them back into ITicketTriage const result = triage.validate(answers); if (result.success === false) { console.error("Evaluation failed:", result.errors); return; } console.log("Triage:", result.data); }; 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, or @hidden properties; arrays of anything but a string literal union or string enum, and array type tags; tuples, dynamic keys, Map, Set, and recursive types; and a leaf without a JSDoc description.

A choice option is described by its enum memberโ€™s JSDoc, or by tags.Constant<V, { description }> on a literal union member, since TypeScript cannot attach JSDoc to a union member. Without either, only the label is sent. A score level without a description is described by its value.

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"].

Validate

validate() takes the providerโ€™s answer map and returns IValidation<T>. 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.

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
V & tags.Probability<N> on a literal union memberthat memberโ€™s acceptance minimum; in an Array<...> set, its inclusion threshold
@probability N on an enum memberthe same as the tag on a literal member
@probability N on a propertythe boolean threshold, or the default minimum (for a set, the default threshold) of the members that carry none

A memberโ€™s own requirement wins over the propertyโ€™s. A choice or score member without either has no minimum, and a set member without either uses the 0.5 threshold, since each set member is its own yes/no decision. Gating only the risky options is the intended use:

gated.ts
interface IDecision { /** What should happen next? */ action: | (tags.Constant<"escalate", { description: "Page the on-call" }> & tags.Probability<0.9>) | tags.Constant<"reply", { description: "Answer the customer" }>; }

When the selected option has a minimum and its probability is below it, validate() fails for that path. It never falls back to a less likely option, because that would invert the modelโ€™s judgment. A gated option also fails when the answer has no probabilities to prove it, which is always the case on the OpenAI, Anthropic, and Google adapters; an ungated option passes without one.

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

TypeSafeโ€™s own SDK and HTTP API spell the boolean question type "noul". Convert the questions with LlmEvaluation.toTypeSafe() from @typia/utils; the answers need no conversion.

typesafe.ts
import { LlmEvaluation } from "@typia/utils"; const response = await client.systemOne({ state: ticket, questions: LlmEvaluation.toTypeSafe(triage.questions), }); const validation = triage.validate(response.answers);

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