Tool Calling Basics
Connect language models to external functions using JSON Schema definitions and structured tool call responses.
TL;DR
- Declare tool functions with descriptive parameter properties in
json-schema. - Detect model invocation requests by checking the
tool_callsarray. - Validate generated function argument payloads using strict
zod-schemas.
Universal Tool Definition
OpenAI Tool DeclarationStructure tool object conforming to OpenAI function calling spec.
const openAiTool = {
type: 'function',
function: {
name: 'search_users',
description: 'Look up user account by email',
parameters: {
type: 'object',
properties: {
email: { type: 'string', format: 'email' },
},
required: ['email'],
},
},
};Zod To JSON SchemaDerive JSON Schema definitions directly from Zod models.
import { zodToJsonSchema } from 'zod-to-json-schema';
import { z } from 'zod';
const UserQuery = z.object({
email: z.string().email(),
});
const schema = zodToJsonSchema(UserQuery, 'UserQuery');Strict Tool Schema FlagEnforce strict JSON schema conformance in OpenAI tools.
const strictTool = {
type: 'function',
function: {
name: 'get_quote',
strict: true,
parameters: { type: 'object' },
},
};Argument Parsing In TypeScript
Safe JSON ParsingSafely deserialize stringified JSON tool call arguments.
function safeParseArgs<T>(raw: string): T | null {
try {
return JSON.parse(raw) as T;
} catch (err) {
logger.error('Malformed tool args', { err });
return null;
}
}Zod Runtime ValidationValidate parsed argument object against strict type contract.
const SearchSchema = z.object({
email: z.string().email(),
});
const parsed = SearchSchema.safeParse(rawJson);
if (!parsed.success) {
throw new Error(
`Validation: ${parsed.error.message}`
);
}Dispatcher Map PatternRoute tool calls to specific handler functions dynamically.
type H = (args: any) => Promise<any>;
const registry: Record<string, H> = {
search_users: handleSearchUsers,
calc_tax: handleCalcTax,
};
const output = await registry[call.name](parsed.data);Tool Conversation Lifecycle
Detect Tool InvocationsCheck if assistant response requested external tool execution.
const calls = res.choices[0].message.tool_calls;
if (calls && calls.length > 0) {
for (const call of calls) {
await processToolCall(call);
}
}Format Tool Message BlockAppend tool execution output to dialogue context array.
const toolMessage = {
role: 'tool',
tool_call_id: call.id,
content: JSON.stringify(toolExecutionResult),
};Dispatch Continuation RequestFeed tool execution message back to model for final answer.
const msgs = [...chatHistory, assistantMsg, toolMsg];
const finalRes = await openai.chat.completions.create({
model: 'gpt-4o',
messages: msgs,
});Defensive Engineering Controls
Tool Execution TimeoutAbort sluggish tool operations after fixed duration ceiling.
async function withTimeout<T>(
p: Promise<T>,
ms = 5000
): Promise<T> {
const timer = new Promise((_, rej) =>
setTimeout(() => rej(new Error('Timeout')), ms)
);
return Promise.race([p, timer]) as Promise<T>;
}Sanitized Error ForwardingProvide actionable error messages so model can recover.
catch (err: any) {
return {
role: 'tool',
tool_call_id: call.id,
content: JSON.stringify({ error: err.message }),
};
}Permission WhitelistingVerify active user possesses permission to invoke tool.
function checkPerm(user: User, toolName: string) {
if (!user.roles.includes(TOOL_PERMS[toolName])) {
throw new Error('Unauthorized tool access');
}
}Tips
- Write detailed descriptions for every
input_schemaparameter to help the model select appropriate tool arguments accurately. - Map tool names to execution handlers using a TypeScript
Record<string, Function>dictionary for clean routing.
Warnings
- Always parse tool call arguments inside a
try-catchblock because models can emit malformed JSON during rare interruptions. - Never run arbitrary shell commands or unsanitized database queries requested by
tool_callswithout rigorous parameter whitelisting.
In Practice
Declares a calculation tool, validates arguments with Zod, and executes request loop.
- Define tool schema with Zod and JSON Schema parameters.
- Dispatch chat completion and inspect tool_calls field.
- Validate arguments with Zod and execute local function.
- Append tool response message to receive final model reply.
import OpenAI from 'openai';
import { z } from 'zod';
const client = new OpenAI();
const Schema = z.object({ sym: z.string() });
const tools = [{
type: 'function' as const,
function: {
name: 'price',
parameters: { type: 'object', properties: {} },
},
}];
const res = await client.chat.completions.create({
model: 'gpt-4o-mini', tools,
messages: [{ role: 'user', content: 'AAPL' }],
});
const call = res.choices[0]?.message?.tool_calls?.[0];
const raw = call?.function?.arguments || '{}';
const args = Schema.parse(JSON.parse(raw));
console.log(args.sym);FAQ
Structured outputs force the model's final user-facing text response to adhere to a schema. Tool calling pauses text generation mid-stream to request external function execution, allowing the model to incorporate results before finalizing its answer.
OpenAI nests definitions inside { type: 'function', function: { name, parameters } }. Anthropic flattens this structure to { name, input_schema }. Both adhere to standard JSON Schema conventions.
Models are probabilistic and can occasionally omit required keys or emit unexpected types. Running schema.parse(args) guarantees type safety and catches bad inputs before invoking business logic.