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.
export namespace llm {
function evaluation<T extends Record<string, any>>(): ILlmEvaluation<T>;
}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
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
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:
TypeScript Source
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 model | evaluation<T>() |
| Generated data of any JSON shape, from a language model | structuredOutput<T>() |
| Function calling, where the LLM picks functions | application<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 type | Question | Value in T |
|---|---|---|
boolean | boolean | true when P(true) reaches the threshold, 0.5 by default |
| string enum, or string literal union | choice | the returned option |
| numeric enum, or numeric literal union | score, levels in ascending value order | the level (see below) |
Array<U> of a string literal union or string enum | one boolean per member | the members decided true |
| nested object | flattened, one question per leaf | the 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:
truewhen 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 fractionalscore, 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.
| Spelling | Meaning |
|---|---|
boolean & tags.Probability<N> | boolean threshold: true only when P(true) โฅ N |
V & tags.Probability<N> on a literal union member | that memberโs acceptance minimum; in an Array<...> set, its inclusion threshold |
@probability N on an enum member | the same as the tag on a literal member |
@probability N on a property | the 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:
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.
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.requestedat0.1next 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.