Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/badlogic/pi-mono/llms.txt

Use this file to discover all available pages before exploring further.

Tool Calling

Tools (also called function calling) enable LLMs to interact with external systems. The library uses TypeBox schemas for type-safe tool definitions with automatic validation.
This library only includes models that support tool calling, as it’s essential for agentic workflows.

Defining Tools

Tools are defined with TypeBox schemas:
import { Type, Tool, StringEnum } from '@mariozechner/pi-ai';

// Simple tool
const timeTool: Tool = {
  name: 'get_time',
  description: 'Get the current time',
  parameters: Type.Object({
    timezone: Type.Optional(Type.String({ 
      description: 'Optional timezone (e.g., America/New_York)' 
    }))
  })
};

// Tool with validation
const weatherTool: Tool = {
  name: 'get_weather',
  description: 'Get current weather for a location',
  parameters: Type.Object({
    location: Type.String({ description: 'City name or coordinates' }),
    units: StringEnum(['celsius', 'fahrenheit'], { default: 'celsius' })
  })
};

// Complex tool
const bookMeetingTool: Tool = {
  name: 'book_meeting',
  description: 'Schedule a meeting',
  parameters: Type.Object({
    title: Type.String({ minLength: 1 }),
    startTime: Type.String({ format: 'date-time' }),
    endTime: Type.String({ format: 'date-time' }),
    attendees: Type.Array(Type.String({ format: 'email' }), { minItems: 1 })
  })
};
For Google API compatibility, use StringEnum helper instead of Type.Enum. Type.Enum generates anyOf/const patterns that Google doesn’t support.

TypeBox Schemas

TypeBox provides rich schema types:
import { Type } from '@mariozechner/pi-ai';

// Basic types
Type.String({ description: 'A string value' })
Type.Number({ minimum: 0, maximum: 100 })
Type.Boolean()
Type.Integer({ minimum: 1 })

// Optional and nullable
Type.Optional(Type.String())
Type.Union([Type.String(), Type.Null()])

// Arrays and objects
Type.Array(Type.String(), { minItems: 1, maxItems: 10 })
Type.Object({
  name: Type.String(),
  age: Type.Integer({ minimum: 0 })
})

// Enums (use StringEnum for Google compatibility)
StringEnum(['option1', 'option2'], { default: 'option1' })

// Advanced
Type.String({ pattern: '^[a-z]+$' })
Type.String({ format: 'email' })
Type.String({ format: 'date-time' })
Type.String({ format: 'uri' })

Basic Tool Usage

import { getModel, complete, Type, Tool, Context } from '@mariozechner/pi-ai';

const tools: Tool[] = [{
  name: 'get_weather',
  description: 'Get current weather',
  parameters: Type.Object({
    location: Type.String({ description: 'City name' })
  })
}];

const context: Context = {
  messages: [{ role: 'user', content: 'What is the weather in London?' }],
  tools
};

const model = getModel('openai', 'gpt-4o-mini');
const response = await complete(model, context);

// Check for tool calls
for (const block of response.content) {
  if (block.type === 'toolCall') {
    console.log(`Tool: ${block.name}`);
    console.log(`Arguments: ${JSON.stringify(block.arguments)}`);
    
    // Execute tool
    const result = await executeWeatherApi(block.arguments);
    
    // Add tool result
    context.messages.push(response);
    context.messages.push({
      role: 'toolResult',
      toolCallId: block.id,
      toolName: block.name,
      content: [{ type: 'text', text: JSON.stringify(result) }],
      isError: false,
      timestamp: Date.now()
    });
  }
}

// Continue conversation if tools were called
if (response.stopReason === 'toolUse') {
  const continuation = await complete(model, context);
  console.log(continuation.content);
}

Tool Results with Images

Tool results support both text and images:
import { readFileSync } from 'fs';

const imageBuffer = readFileSync('chart.png');

context.messages.push({
  role: 'toolResult',
  toolCallId: 'tool_xyz',
  toolName: 'generate_chart',
  content: [
    { type: 'text', text: 'Generated chart showing temperature trends' },
    { 
      type: 'image', 
      data: imageBuffer.toString('base64'), 
      mimeType: 'image/png' 
    }
  ],
  isError: false,
  timestamp: Date.now()
});

Validating Tool Arguments

Use validateToolCall to validate arguments against schemas:
import { stream, validateToolCall, Tool } from '@mariozechner/pi-ai';

const tools: Tool[] = [weatherTool, calculatorTool];
const s = stream(model, { messages, tools });

for await (const event of s) {
  if (event.type === 'toolcall_end') {
    const toolCall = event.toolCall;
    
    try {
      // Validate arguments (throws on invalid args)
      const validatedArgs = validateToolCall(tools, toolCall);
      const result = await executeMyTool(toolCall.name, validatedArgs);
      
      context.messages.push({
        role: 'toolResult',
        toolCallId: toolCall.id,
        toolName: toolCall.name,
        content: [{ type: 'text', text: JSON.stringify(result) }],
        isError: false,
        timestamp: Date.now()
      });
    } catch (error) {
      // Validation failed - return error so model can retry
      context.messages.push({
        role: 'toolResult',
        toolCallId: toolCall.id,
        toolName: toolCall.name,
        content: [{ type: 'text', text: error.message }],
        isError: true,
        timestamp: Date.now()
      });
    }
  }
}

Streaming Tool Calls

Tool arguments are streamed and progressively parsed:
import { stream, Type, Tool } from '@mariozechner/pi-ai';

const tools: Tool[] = [{
  name: 'write_file',
  description: 'Write content to a file',
  parameters: Type.Object({
    path: Type.String({ description: 'File path' }),
    content: Type.String({ description: 'File content' })
  })
}];

const s = stream(model, { messages, tools });

for await (const event of s) {
  if (event.type === 'toolcall_start') {
    console.log(`[Tool call started: index ${event.contentIndex}]`);
  }
  
  if (event.type === 'toolcall_delta') {
    const toolCall = event.partial.content[event.contentIndex];
    
    // BE DEFENSIVE: arguments may be incomplete during streaming
    if (toolCall.type === 'toolCall' && toolCall.arguments) {
      if (toolCall.name === 'write_file') {
        // Show file path as soon as it's available
        if (toolCall.arguments.path) {
          console.log(`Writing to: ${toolCall.arguments.path}`);
        }
        
        // Content might be partial or missing
        if (toolCall.arguments.content) {
          console.log(`Content preview: ${toolCall.arguments.content.substring(0, 100)}...`);
        }
      }
    }
  }
  
  if (event.type === 'toolcall_end') {
    // Arguments are now complete (but not yet validated)
    const toolCall = event.toolCall;
    console.log(`Tool completed: ${toolCall.name}`);
    console.log(`Full arguments:`, toolCall.arguments);
  }
}
Important notes about partial arguments:
  • During toolcall_delta, arguments contains best-effort parse of partial JSON
  • Fields may be missing, incomplete, or truncated mid-word
  • String values may be cut off mid-sentence
  • Arrays and objects may be partially populated
  • At minimum, arguments will be {}, never undefined
  • Google provider does not support streaming tool calls - you get one toolcall_delta with full arguments

Multiple Tool Calls

Models can call multiple tools in one response:
const response = await complete(model, context);

// Process all tool calls
for (const block of response.content) {
  if (block.type === 'toolCall') {
    let result;
    
    switch (block.name) {
      case 'get_weather':
        result = await getWeather(block.arguments.location);
        break;
      case 'get_time':
        result = await getTime(block.arguments.timezone);
        break;
      default:
        result = { error: `Unknown tool: ${block.name}` };
    }
    
    context.messages.push({
      role: 'toolResult',
      toolCallId: block.id,
      toolName: block.name,
      content: [{ type: 'text', text: JSON.stringify(result) }],
      isError: false,
      timestamp: Date.now()
    });
  }
}

// Add assistant message to context
context.messages.push(response);

// Continue to get final response
if (response.stopReason === 'toolUse') {
  const finalResponse = await complete(model, context);
}

Tool Choice

Control when tools are called:
// Model decides when to use tools
const response = await complete(model, context, {
  toolChoice: 'auto'
});

Error Handling

Handle tool execution errors:
for (const block of response.content) {
  if (block.type === 'toolCall') {
    try {
      // Validate arguments
      const validatedArgs = validateToolCall(tools, block);
      
      // Execute tool
      const result = await executeTool(block.name, validatedArgs);
      
      context.messages.push({
        role: 'toolResult',
        toolCallId: block.id,
        toolName: block.name,
        content: [{ type: 'text', text: JSON.stringify(result) }],
        isError: false,
        timestamp: Date.now()
      });
    } catch (error) {
      // Return error to model
      context.messages.push({
        role: 'toolResult',
        toolCallId: block.id,
        toolName: block.name,
        content: [{ 
          type: 'text', 
          text: `Error: ${error.message}` 
        }],
        isError: true,
        timestamp: Date.now()
      });
    }
  }
}

context.messages.push(response);

// Model will see the error and can retry or handle it
const continuation = await complete(model, context);

Real-World Example

Complete tool calling workflow:
import { getModel, complete, validateToolCall, Type, Tool, Context } from '@mariozechner/pi-ai';
import { readFileSync, writeFileSync } from 'fs';
import { exec } from 'child_process';
import { promisify } from 'util';

const execAsync = promisify(exec);

// Define tools
const tools: Tool[] = [
  {
    name: 'read_file',
    description: 'Read contents of a file',
    parameters: Type.Object({
      path: Type.String({ description: 'File path' })
    })
  },
  {
    name: 'write_file',
    description: 'Write content to a file',
    parameters: Type.Object({
      path: Type.String({ description: 'File path' }),
      content: Type.String({ description: 'File content' })
    })
  },
  {
    name: 'run_command',
    description: 'Run a shell command',
    parameters: Type.Object({
      command: Type.String({ description: 'Shell command to run' })
    })
  }
];

// Tool execution functions
const toolFunctions = {
  read_file: async (args: { path: string }) => {
    const content = readFileSync(args.path, 'utf-8');
    return { content };
  },
  write_file: async (args: { path: string; content: string }) => {
    writeFileSync(args.path, args.content);
    return { success: true };
  },
  run_command: async (args: { command: string }) => {
    const { stdout, stderr } = await execAsync(args.command);
    return { stdout, stderr };
  }
};

// Main loop
const model = getModel('openai', 'gpt-4o-mini');
const context: Context = {
  systemPrompt: 'You are a helpful coding assistant.',
  messages: [
    { role: 'user', content: 'Create a Python script that prints "Hello, World!"' }
  ],
  tools
};

let maxIterations = 10;

while (maxIterations-- > 0) {
  const response = await complete(model, context);
  context.messages.push(response);
  
  // Check if we're done
  if (response.stopReason === 'stop') {
    for (const block of response.content) {
      if (block.type === 'text') {
        console.log(block.text);
      }
    }
    break;
  }
  
  // Execute tool calls
  if (response.stopReason === 'toolUse') {
    for (const block of response.content) {
      if (block.type === 'toolCall') {
        try {
          // Validate arguments
          const validatedArgs = validateToolCall(tools, block);
          
          // Execute tool
          const toolFn = toolFunctions[block.name as keyof typeof toolFunctions];
          if (!toolFn) {
            throw new Error(`Unknown tool: ${block.name}`);
          }
          
          console.log(`Executing: ${block.name}(${JSON.stringify(validatedArgs)})`);
          const result = await toolFn(validatedArgs as any);
          
          context.messages.push({
            role: 'toolResult',
            toolCallId: block.id,
            toolName: block.name,
            content: [{ type: 'text', text: JSON.stringify(result) }],
            isError: false,
            timestamp: Date.now()
          });
        } catch (error: any) {
          context.messages.push({
            role: 'toolResult',
            toolCallId: block.id,
            toolName: block.name,
            content: [{ type: 'text', text: `Error: ${error.message}` }],
            isError: true,
            timestamp: Date.now()
          });
        }
      }
    }
    continue;
  }
  
  // Handle errors
  if (response.stopReason === 'error' || response.stopReason === 'aborted') {
    console.error('Error:', response.errorMessage);
    break;
  }
}

if (maxIterations === -1) {
  console.error('Max iterations reached');
}

Provider-Specific Notes

Google

  • Does not support streaming tool arguments
  • Receives single toolcall_delta event with complete arguments
  • Use StringEnum instead of Type.Enum for enums

Mistral

  • Requires tool call IDs to be exactly 9 alphanumeric characters
  • Library handles this automatically via normalization

OpenRouter

  • Tool support depends on underlying model
  • Some models may not support all tool features

Custom Models

  • Check model.api to determine tool format
  • OpenAI-compatible APIs use OpenAI tool format
  • Anthropic API uses Anthropic tool format
  • Google APIs use Google function calling format

Next Steps

Streaming

Learn about streaming tool calls

Thinking

Combine tools with reasoning