LangChain.js
@typia/langchain plugs typia controllers into LangChain.jsย . Every method on your TypeScript class or every endpoint in an OpenAPI document becomes a DynamicStructuredTool ready for AgentExecutor and friends, with the same parse/coerce/validate/feedback machinery as typia.llm.application wired in automatically.
import { toLangChainTools } from "@typia/langchain";
export function toLangChainTools(
controller: ILlmController | IHttpLlmController,
options?: { prefix?: boolean },
): DynamicStructuredTool[];
export function toLangChainTools(
controllers: Array<ILlmController | IHttpLlmController>,
options?: { prefix?: boolean },
): DynamicStructuredTool[];
export function toLangChainTools(props: {
controllers: Array<ILlmController | IHttpLlmController>;
prefix?: boolean; // default false; if true, tool names are "{controller}_{method}"
}): DynamicStructuredTool[];undefined
export function toLangChainTools(
controller: ILlmController | IHttpLlmController,
options?: { prefix?: boolean },
): DynamicStructuredTool[];
export function toLangChainTools(
controllers: Array<ILlmController | IHttpLlmController>,
options?: { prefix?: boolean },
): DynamicStructuredTool[];
export function toLangChainTools(props: {
controllers: Array<ILlmController | IHttpLlmController>;
prefix?: boolean | undefined;
}): DynamicStructuredTool[];Setup
npm install @typia/langchain @langchain/core
npm install typia
npm install -D ttsc typescriptFrom a TypeScript class
import { ChainValues, Runnable } from "@langchain/core";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { DynamicStructuredTool } from "@langchain/core/tools";
import { ChatOpenAI } from "@langchain/openai";
import { toLangChainTools } from "@typia/langchain";
import { AgentExecutor, createToolCallingAgent } from "langchain/agents";
import typia from "typia";
import { Calculator } from "./Calculator";
const tools: DynamicStructuredTool[] = toLangChainTools(
typia.llm.controller<Calculator>("calculator", new Calculator()),
);
const agent: Runnable = createToolCallingAgent({
llm: new ChatOpenAI({ model: "gpt-4o" }),
tools,
prompt: ChatPromptTemplate.fromMessages([
["system", "You are a helpful assistant."],
["human", "{input}"],
["placeholder", "{agent_scratchpad}"],
]),
});
const executor: AgentExecutor = new AgentExecutor({ agent, tools });
const result: ChainValues = await executor.invoke({
input: "What is 10 + 5?",
});Every method on Calculator is now a LangChain tool. JSDoc comments become tool descriptions, TypeScript types become JSON schemas. Tool names default to the bare method name; pass { prefix: true } as the second argument to toLangChainTools to get {controllerName}_{methodName}. Final tool names must be unique even with prefixes enabled, so controllers that still emit the same name are rejected before registration. The older toLangChainTools({ controllers: [...] }) form still works when that shape is clearer for multiple controllers.
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 { DynamicStructuredTool } from "@langchain/core/tools";
import { toLangChainTools } from "@typia/langchain";
import { HttpLlm } from "@typia/utils";
const tools: DynamicStructuredTool[] = toLangChainTools(
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 ********" },
},
}),
);The function calling harness
Every tool carries the harness: type coercion, validation, and model-readable feedback. When typia validation catches invalid arguments, the tool raises ToolInputParsingException with the original input annotated by // โ markers:
{
"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 see LlmJson.
Why LangChain doesnโt validate first
A LangChain toolโs schema normally does two jobs: it becomes the parameters the model is shown, and LangChain validates arguments against it with @cfworker/json-schema before the tool body runs. That second job would preempt typia โ a stringified "42" would be rejected instead of coerced to 42, and the model would get LangChainโs Received tool input did not match expected schema instead of the annotated feedback above.
So @typia/langchain registers the parameters as a Standard JSON Schemaย : a schema for the model, with no validator attached. LangChain reads it back through its own toJsonSchema, so the model sees the same parameters document either way, and typiaโs coerce-then-validate path is the only one that runs. @typia/vercel states the same contract to the AI SDK through jsonSchema(), whose absent validate makes the SDK skip its own check; @typia/mcp needs no such statement, because the low-level MCP server never validates a callโs arguments for itself. Byte-identical arguments now get byte-identical answers from all three.
This is why @typia/langchain requires @langchain/core@1.1.30 or newer โ that is the release whose toJsonSchema reads a Standard JSON Schema back.
Runtime errors
There are two distinct failure modes:
- Validation error: the LLM passed arguments that donโt match the schema. The tool raises
ToolInputParsingExceptionwith validation feedback. - Runtime error: the tool itself threw, for example a divide-by-zero, network error, or business-rule violation.
@typia/langchain catches runtime errors after argument validation and returns { success: false, error: "..." }. That keeps the agent loop alive while preserving the original error message for the model.
Structured output
For LangChainโs withStructuredOutput, hand it the schema from typia.llm.parameters and validate the result with typia.validate:
import { ChatOpenAI } from "@langchain/openai";
import { dedent, LlmJson } from "@typia/utils";
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 model = new ChatOpenAI({ model: "gpt-4o" })
.withStructuredOutput(typia.llm.parameters<IMember>());
const member: IMember = await model.invoke(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.
`);
const result = typia.validate<IMember>(member);
if (!result.success) {
console.error(LlmJson.stringify(result));
// โ send `LlmJson.stringify(result)` back to the model for correction
}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. The schema and the validator both come from it; the feedback loop closes the gap between what the model produced and what your code expected.
Where to go next
- Source TypeScript class for the tools:
typia.llm.application - Source OpenAPI document for the tools:
HttpLlm - Harness internals:
LlmJson - Same idea, different framework: Vercel AI SDK and MCP
- Full agent loop on top: Agentica