Vercel AI SDK
@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(props: {
controllers: Array<ILlmController | IHttpLlmController>;
prefix?: boolean; // default false; if true, tool names are "{controller}_{method}"
}): Record<string, Tool>;undefined
export function toVercelTools(props: {
controllers: Array<ILlmController | IHttpLlmController>;
prefix?: boolean | undefined;
}): Record<string, Tool>;Setup
npm install @typia/vercel ai
npm install typia
npx typia setupFrom 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({
controllers: [
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. JSDoc comments become tool descriptions, TypeScript types become JSON schemas. Tool names default to the bare method name; pass prefix: true to toVercelTools to get {controllerName}_{methodName} instead.
undefined
export class Calculator {
/**
* Add two numbers.
*
* @param p The input containing two numbers to add
* @returns The sum of a and b
*/
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 a and b
*/
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 a and b
*/
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 a and b
*/
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 {
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({
controllers: [
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 { error: true, message: "..." }. 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";
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");
});
};