Build / AI /

Tool Calling Basics

Connect language models to external functions using JSON Schema definitions and structured tool call responses.

TL;DR

  1. Declare tool functions with descriptive parameter properties in json-schema.
  2. Detect model invocation requests by checking the tool_calls array.
  3. Validate generated function argument payloads using strict zod-schemas.

Universal Tool Definition

    OpenAI Tool Declaration

    Structure 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 Schema

    Derive 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 Flag

    Enforce 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 Parsing

    Safely 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 Validation

    Validate 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 Pattern

    Route 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 Invocations

    Check 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 Block

    Append tool execution output to dialogue context array.

    const toolMessage = {
      role: 'tool',
      tool_call_id: call.id,
      content: JSON.stringify(toolExecutionResult),
    };
    Dispatch Continuation Request

    Feed 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 Timeout

    Abort 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 Forwarding

    Provide 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 Whitelisting

    Verify 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

  1. Write detailed descriptions for every input_schema parameter to help the model select appropriate tool arguments accurately.
  2. Map tool names to execution handlers using a TypeScript Record<string, Function> dictionary for clean routing.

Warnings

  1. Always parse tool call arguments inside a try-catch block because models can emit malformed JSON during rare interruptions.
  2. Never run arbitrary shell commands or unsanitized database queries requested by tool_calls without rigorous parameter whitelisting.

In Practice

FAQ