Vercel AI SDK
For AI SDK 7โs experimental_evaluate(), use typia.llm.evaluation<T>() directly. Its questions already have the AI SDK shape, and its decode(result.answers) converts the answers to T; @typia/vercel is not involved.
@typia/vercel plugs typia controllers into the Vercel AI SDKย . Every method on your TypeScript class โ or every endpoint in an OpenAPI document โ becomes a Tool ready for generateText/streamText, with the same parse/coerce/validate/feedback machinery as typia.llm.application wired in automatically.
import { toVercelTools } from "@typia/vercel";
export function toVercelTools(
controller: ILlmController | IHttpLlmController,
options?: { prefix?: boolean },
): Record<string, Tool>;
export function toVercelTools(
controllers: Array<ILlmController | IHttpLlmController>,
options?: { prefix?: boolean },
): Record<string, Tool>;
export function toVercelTools(props: {
controllers: Array<ILlmController | IHttpLlmController>;
prefix?: boolean; // default false; if true, tool names are "{controller}_{method}"
}): Record<string, Tool>;undefined
export function toVercelTools(
controller: ILlmController | IHttpLlmController,
options?: { prefix?: boolean },
): Record<string, Tool>;
export function toVercelTools(
controllers: Array<ILlmController | IHttpLlmController>,
options?: { prefix?: boolean },
): Record<string, Tool>;
export function toVercelTools(props: {
controllers: Array<ILlmController | IHttpLlmController>;
prefix?: boolean | undefined;
}): Record<string, Tool>;Setup
npm install @typia/vercel ai
npm install typia
npm install -D ttsc typescriptFrom a TypeScript class
export class Calculator {
/** Add two numbers. Returns their sum. */
add(props: { x: number; y: number }): { value: number } {
return { value: props.x + props.y };
}
/** Multiply two numbers. Returns their product. */
multiply(props: { x: number; y: number }): { value: number } {
return { value: props.x * props.y };
}
}import { openai } from "@ai-sdk/openai";
import { toVercelTools } from "@typia/vercel";
import { generateText, GenerateTextResult, Tool } from "ai";
import typia from "typia";
import { Calculator } from "./Calculator";
const tools: Record<string, Tool> = toVercelTools(
typia.llm.controller<Calculator>("calculator", new Calculator()),
);
const result: GenerateTextResult = await generateText({
model: openai("gpt-4o"),
prompt: "What is 10 + 5?",
tools,
});The JSDoc on each method becomes the tool description; the parameter type becomes the JSON schema. Every method on Calculator is now a Vercel Tool. Tool names default to the bare method name; pass { prefix: true } as the second argument to toVercelTools to get {controllerName}_{methodName} instead. Final tool names must be unique even with prefixes enabled, so controllers that still emit the same name are rejected before registration. The older toVercelTools({ controllers: [...] }) form still works when that shape is clearer for multiple controllers. When a method has a reflected return type, the tool also advertises an outputSchema for the { success, data/error } result wrapper returned by execute.
execute validates each controller result against the reflected return schema before using the success branch. Missing fields, wrong nested types, additional properties, and non-object values return { success: false, error } with annotated output paths, so every result still conforms to the advertised wrapper schema. A method declared as void keeps the schema-free { success: true } result.
undefined
/**
* Arithmetic controller fixture for reflected tools and SDK mock calls.
*
* Numeric operands and object results keep dispatch observable; divide throws
* at a zero denominator so both direct and SDK execution can test feedback.
*/
export class Calculator {
/**
* Add two numbers.
*
* @param p The input containing two numbers to add
*
* @returns The sum of x and y
*/
add(p: Calculator.IProps): Calculator.IResult {
return { value: p.x + p.y };
}
/**
* Subtract two numbers.
*
* @param p The input containing two numbers to subtract
*
* @returns The difference of x and y
*/
subtract(p: Calculator.IProps): Calculator.IResult {
return { value: p.x - p.y };
}
/**
* Multiply two numbers.
*
* @param p The input containing two numbers to multiply
*
* @returns The product of x and y
*/
multiply(p: Calculator.IProps): Calculator.IResult {
return { value: p.x * p.y };
}
/**
* Divide two numbers.
*
* @param p The input containing two numbers to divide
*
* @returns The quotient of x and y
*/
divide(p: Calculator.IProps): Calculator.IResult {
if (p.y === 0) {
throw new Error("Division by zero is not allowed");
}
return { value: p.x / p.y };
}
}
export namespace Calculator {
/** Numeric operands for the four arithmetic fixture methods. */
export interface IProps {
/** First operand */
x: number;
/** Second operand */
y: number;
}
/** Result of a calculation. */
export interface IResult {
/** Calculated value */
value: number;
}
}Method type rules. Every methodโs parameter type must be a keyworded object with static keys (no primitives, arrays, or unions). The return type must be an object or void. See typia.llm.application restrictions for the full list.
From an OpenAPI document
For REST APIs documented with Swagger / OpenAPI, swap HttpLlm.controller in:
import { toVercelTools } from "@typia/vercel";
import { HttpLlm } from "@typia/utils";
import { Tool } from "ai";
const tools: Record<string, Tool> = toVercelTools(
HttpLlm.controller({
name: "shopping",
document: await fetch(
"https://shopping-be.wrtn.ai/editor/swagger.json",
).then((r) => r.json()),
connection: {
host: "https://shopping-be.wrtn.ai",
headers: { Authorization: "Bearer ********" },
},
}),
);Every API operation becomes a tool with parameters, descriptions, and the same harness wrapping each call.
The function calling harness
Every tool produced by toVercelTools carries lenient JSON parsing, type coercion, and validation feedback. When validation fails, the tool returns the original input annotated with inline // โ markers โ the LLM reads it and self-corrects on the next turn:
{
"name": "John",
"age": "twenty", // โ [{"path":"$input.age","expected":"number"}]
"email": "not-an-email", // โ [{"path":"$input.email","expected":"string & Format<\"email\">"}]
"hobbies": "reading" // โ [{"path":"$input.hobbies","expected":"Array<string>"}]
}For the mechanics of parse / coerce / validate / stringify, see LlmJson.
Structured output
For Vercelโs generateObject (i.e. structured output, no function-picking involved), use typia.llm.parameters<T>() plus a validate callback that runs typia.validate:
import { openai } from "@ai-sdk/openai";
import { dedent, LlmJson } from "@typia/utils";
import { generateObject, jsonSchema } from "ai";
import typia, { tags } from "typia";
interface IMember {
email: string & tags.Format<"email">;
name: string;
age: number & tags.Minimum<0> & tags.Maximum<100>;
hobbies: string[];
joined_at: string & tags.Format<"date">;
}
const { object } = await generateObject({
model: openai("gpt-4o"),
schema: jsonSchema<IMember>(typia.llm.parameters<IMember>(), {
validate: (value) => {
const result = typia.validate<IMember>(value);
if (result.success) return { success: true, value: result.data };
return {
success: false,
error: new Error(LlmJson.stringify(result)),
};
},
}),
prompt: dedent`
I am a new member of the community.
My name is John Doe, and I am 25 years old.
I like playing basketball and reading books,
and joined to this community at 2022-01-01.
`,
});Terminal{ email: 'john.doe@example.com', name: 'John Doe', age: 25, hobbies: [ 'playing basketball', 'reading books' ], joined_at: '2022-01-01' }
The IMember interface is the single source of truth. typia.llm.parameters<IMember>() produces the schema, typia.validate<IMember>() checks the result, and LlmJson.stringify formats any validation failure for the LLM to self-correct on retry.
Runtime errors
There are two distinct failure modes:
- Validation error โ the LLM passed arguments that donโt match the schema. Tool returns the annotated input so the model can fix it.
- Runtime error โ the tool itself threw (e.g. divide-by-zero, network error, business-rule violation).
@typia/vercel catches the runtime error and surfaces it as { success: false, error: "..." }. That keeps the conversation alive โ the model can see the failure and either retry with different inputs or apologize to the user.
Error handling test
import { TestValidator } from "@nestia/e2e";
import { ILlmController } from "@typia/interface";
import { toVercelTools } from "@typia/vercel";
import type { Tool } from "ai";
import typia from "typia";
import { Calculator } from "../structures/Calculator";
/**
* Verifies vercel class controller error handling against the native
* typia.llm.controller output.
*
* The case builds its input in this file and asserts result should be a failure
* object, error should contain division by zero.
*
* 1. Generate the value from the types declared in this file.
* 2. Assert the properties listed above.
*
* @evidence contracts/testing.md#behavioral-verification Executes the transformed Calculator divide tool with x=10,y=0 and asserts failure plus the declared Division by zero message.
* @evidence contracts/testing.md#independent-expectations Calculator.divide deliberately throws when y is zero; the adapter contract returns success:false with actionable error text instead of rejecting its tool Promise.
* @evidence contracts/testing.md#distinguishing-cases The zero denominator is the runtime-exception branch; class_controller_execute owns a valid 20/4 division through the same adapter.
* @evidence contracts/testing.md#execution-ownership DynamicExecutor discovers test_vercel_class_controller_error_handling in src/features through the native-enabled integration command. Private fixture classes and local callbacks are reviewed through this entry.
* @evidence contracts/e2e.md#necessary-boundary The native producer's divide metadata and controller executor connect through the adapter; zero-denominator execution must return its authored exception as failure feedback. class_controller_execute is the valid nonzero twin.
* @evidence contracts/e2e.md#shared-execution All feature declarations belong to the same test-vercel project and ttsx integration invocation; native plugin preparation is shared rather than rebuilt per case. SDK mock models are lightweight per-case protocol inputs, not independent compiler projects.
* @evidence contracts/e2e.md#state-isolation-and-reuse-validity The suite reuses ttsc's native binary keyed by plugin source/dependencies and the same project compilation; changed plugin inputs invalidate the key. This invocation owns fresh fixture or harness objects and any mock response/counter state, opens no network host and awaits all execution before returning. No case-owned process or handle survives assertion failure.
* @evidence contracts/e2e.md#preserved-coverage Every original input, assertion and exported case name remains in this feature. Portable HTTP registration/output cases are separately retained in the plugin-free unit population; no runtime assertion is replaced by source text or emitted-helper presence.
*/
export const test_vercel_class_controller_error_handling =
async (): Promise<void> => {
// 1. Create class-based controller using typia.llm.controller
const controller: ILlmController<Calculator> =
typia.llm.controller<Calculator>("calculator", new Calculator());
// 2. Convert to Vercel tools
const tools: Record<string, Tool> = toVercelTools({
controllers: [controller],
});
// 3. Test divide by zero (throws an error)
const divideTool: Tool = tools["divide"]!;
const result: unknown = await divideTool.execute!(
{ x: 10, y: 0 },
{ toolCallId: "test-1", messages: [], abortSignal: undefined as any },
);
// 4. Verify the result contains error
TestValidator.predicate("result should be a failure object", () => {
const res = result as { success?: boolean; error?: string };
return res.success === false && typeof res.error === "string";
});
TestValidator.predicate("error should contain division by zero", () => {
const res = result as { success: boolean; error: string };
return res.error.includes("Division by zero");
});
};