Focused documentation on customizing tool execution when importing tools from npm packages. Covers 10 patterns: - Simple override with spread - Wrap with pre/post processing - Transform inputs/outputs - Add authentication/API keys - Conditional execution - Retry logic - Timeout handling - Rate limiting - Validation layer - Wrapper factory for reusable patterns Includes TypeScript typing guidance and real-world examples. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
14 KiB
Overriding Execute Functions for npm Tools
When you import an AI SDK tool from npm, it comes with a built-in execute function. This guide shows how to override or extend that execution before passing the tool to generateText or streamText.
The Problem
You install a tool from npm:
npm install @tpmjs/some-tool
And import it:
import { someTool } from '@tpmjs/some-tool';
import { generateText } from 'ai';
const result = await generateText({
model: openai('gpt-4o'),
tools: { someTool }, // Uses the package's built-in execute
prompt: 'Do something',
});
But what if you want to:
- Add logging before/after execution
- Add authentication or API keys
- Cache results
- Transform inputs or outputs
- Replace the execution entirely
- Add rate limiting or retries
Understanding Tool Structure
An AI SDK tool is an object with this shape:
type Tool = {
description: string;
parameters: ZodSchema;
execute: (args: T, options: ToolExecuteOptions) => Promise<Result>;
};
The key insight: tools are just objects. You can spread them, override properties, and create new tools from existing ones.
Pattern 1: Simple Override with Spread
The most straightforward approach - spread the original tool and override execute:
import { someTool } from '@tpmjs/some-tool';
import { generateText } from 'ai';
const myTool = {
...someTool,
execute: async (args, options) => {
console.log('Custom execution with args:', args);
// Your completely custom implementation
return { result: 'my custom result' };
},
};
const result = await generateText({
model: openai('gpt-4o'),
tools: { myTool },
prompt: 'Do something',
});
Use when: You want to completely replace the execution logic.
Pattern 2: Wrap with Pre/Post Processing
Call the original execute but add behavior before and after:
import { someTool } from '@tpmjs/some-tool';
const wrappedTool = {
...someTool,
execute: async (args, options) => {
// PRE-PROCESSING
console.log(`[${new Date().toISOString()}] Calling tool with:`, args);
const startTime = Date.now();
try {
// CALL ORIGINAL
const result = await someTool.execute(args, options);
// POST-PROCESSING
const duration = Date.now() - startTime;
console.log(`[${duration}ms] Tool returned:`, result);
return result;
} catch (error) {
console.error('Tool execution failed:', error);
throw error;
}
},
};
Use when: You want to keep the original behavior but add logging, metrics, or transformations.
Pattern 3: Transform Inputs
Modify arguments before they reach the original execute:
import { searchTool } from '@tpmjs/search-tool';
const enhancedSearchTool = {
...searchTool,
execute: async (args, options) => {
// Transform inputs
const enhancedArgs = {
...args,
query: `${args.query} site:example.com`, // Add search filter
limit: Math.min(args.limit || 10, 50), // Cap results
};
return searchTool.execute(enhancedArgs, options);
},
};
Pattern 4: Transform Outputs
Modify the result before returning:
import { dataTool } from '@tpmjs/data-tool';
const formattedDataTool = {
...dataTool,
execute: async (args, options) => {
const result = await dataTool.execute(args, options);
// Transform output
return {
...result,
data: result.data.map(item => ({
...item,
timestamp: new Date(item.timestamp).toLocaleString(),
})),
_meta: {
fetchedAt: new Date().toISOString(),
source: 'dataTool',
},
};
},
};
Pattern 5: Add Authentication/API Keys
Inject credentials that the original tool needs:
import { apiTool } from '@tpmjs/api-tool';
const authenticatedTool = {
...apiTool,
execute: async (args, options) => {
// Inject API key into args
const argsWithAuth = {
...args,
apiKey: process.env.EXTERNAL_API_KEY,
headers: {
...args.headers,
'Authorization': `Bearer ${process.env.AUTH_TOKEN}`,
},
};
return apiTool.execute(argsWithAuth, options);
},
};
Pattern 6: Conditional Execution
Route to different implementations based on conditions:
import { defaultTool } from '@tpmjs/default-tool';
const conditionalTool = {
...defaultTool,
execute: async (args, options) => {
// Use mock in development
if (process.env.NODE_ENV === 'development') {
return { result: 'mock data', mocked: true };
}
// Use cached result if available
const cacheKey = JSON.stringify(args);
const cached = await cache.get(cacheKey);
if (cached) {
return { ...cached, fromCache: true };
}
// Otherwise use original
const result = await defaultTool.execute(args, options);
await cache.set(cacheKey, result, { ttl: 3600 });
return result;
},
};
Pattern 7: Add Retry Logic
Wrap execution with automatic retries:
import { unreliableTool } from '@tpmjs/unreliable-tool';
const reliableTool = {
...unreliableTool,
execute: async (args, options) => {
const maxRetries = 3;
let lastError: Error;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await unreliableTool.execute(args, options);
} catch (error) {
lastError = error as Error;
console.warn(`Attempt ${attempt} failed:`, error.message);
if (attempt < maxRetries) {
// Exponential backoff
await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 100));
}
}
}
throw new Error(`Failed after ${maxRetries} attempts: ${lastError.message}`);
},
};
Pattern 8: Add Timeout
Prevent tools from hanging:
import { slowTool } from '@tpmjs/slow-tool';
const timedTool = {
...slowTool,
execute: async (args, options) => {
const timeoutMs = 10000; // 10 seconds
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Tool execution timed out')), timeoutMs);
});
return Promise.race([
slowTool.execute(args, options),
timeoutPromise,
]);
},
};
Pattern 9: Rate Limiting
Control how often a tool can be called:
import { expensiveTool } from '@tpmjs/expensive-tool';
// Simple rate limiter
const rateLimiter = {
calls: [] as number[],
maxCalls: 10,
windowMs: 60000, // 1 minute
async acquire() {
const now = Date.now();
this.calls = this.calls.filter(t => now - t < this.windowMs);
if (this.calls.length >= this.maxCalls) {
const oldestCall = this.calls[0];
const waitTime = this.windowMs - (now - oldestCall);
await new Promise(r => setTimeout(r, waitTime));
return this.acquire();
}
this.calls.push(now);
},
};
const rateLimitedTool = {
...expensiveTool,
execute: async (args, options) => {
await rateLimiter.acquire();
return expensiveTool.execute(args, options);
},
};
Pattern 10: Validation Layer
Add input validation before execution:
import { unsafeTool } from '@tpmjs/unsafe-tool';
import { z } from 'zod';
// Additional validation schema
const strictSchema = z.object({
query: z.string().min(1).max(1000),
options: z.object({
limit: z.number().int().min(1).max(100),
}).optional(),
});
const validatedTool = {
...unsafeTool,
execute: async (args, options) => {
// Validate before execution
const validated = strictSchema.parse(args);
// Sanitize
const sanitized = {
...validated,
query: validated.query.replace(/<[^>]*>/g, ''), // Strip HTML
};
return unsafeTool.execute(sanitized, options);
},
};
Creating a Wrapper Factory
For reusable patterns, create a factory function:
type WrapperOptions<T, R> = {
before?: (args: T) => T | Promise<T>;
after?: (result: R, args: T) => R | Promise<R>;
onError?: (error: Error, args: T) => R | Promise<R>;
timeout?: number;
retries?: number;
};
function wrapTool<T, R>(
tool: { description: string; parameters: any; execute: (args: T, opts: any) => Promise<R> },
options: WrapperOptions<T, R> = {}
) {
return {
...tool,
execute: async (args: T, execOptions: any): Promise<R> => {
// Transform inputs
let processedArgs = args;
if (options.before) {
processedArgs = await options.before(args);
}
// Execute with timeout
const executeWithTimeout = async () => {
if (options.timeout) {
return Promise.race([
tool.execute(processedArgs, execOptions),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), options.timeout)
),
]);
}
return tool.execute(processedArgs, execOptions);
};
// Execute with retries
let lastError: Error | undefined;
const maxAttempts = (options.retries ?? 0) + 1;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
let result = await executeWithTimeout();
// Transform outputs
if (options.after) {
result = await options.after(result, processedArgs);
}
return result;
} catch (error) {
lastError = error as Error;
if (attempt < maxAttempts) {
await new Promise(r => setTimeout(r, Math.pow(2, attempt) * 100));
}
}
}
// Handle error
if (options.onError) {
return options.onError(lastError!, processedArgs);
}
throw lastError;
},
};
}
// Usage
import { someTool } from '@tpmjs/some-tool';
const enhancedTool = wrapTool(someTool, {
before: (args) => ({ ...args, enhanced: true }),
after: (result) => ({ ...result, processedAt: new Date() }),
timeout: 5000,
retries: 2,
onError: (error, args) => ({ error: error.message, args, fallback: true }),
});
Using with generateText/streamText
All patterns work the same way with the AI SDK:
import { generateText, streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
import { originalTool } from '@tpmjs/original-tool';
// Create your wrapped version
const myTool = {
...originalTool,
execute: async (args, options) => {
// Your custom logic
return originalTool.execute(args, options);
},
};
// Use with generateText
const result = await generateText({
model: openai('gpt-4o'),
tools: { myTool },
maxSteps: 5,
prompt: 'Use the tool to do something',
});
// Or with streamText
const stream = streamText({
model: openai('gpt-4o'),
tools: { myTool },
maxSteps: 5,
prompt: 'Use the tool to do something',
});
for await (const chunk of stream.textStream) {
process.stdout.write(chunk);
}
Combining Multiple Tools
Override multiple tools at once:
import { toolA } from '@tpmjs/tool-a';
import { toolB } from '@tpmjs/tool-b';
import { toolC } from '@tpmjs/tool-c';
// Logging wrapper for all tools
function withLogging<T extends Record<string, any>>(tools: T): T {
const wrapped: any = {};
for (const [name, tool] of Object.entries(tools)) {
wrapped[name] = {
...tool,
execute: async (args: any, options: any) => {
console.log(`[${name}] called with:`, args);
const result = await tool.execute(args, options);
console.log(`[${name}] returned:`, result);
return result;
},
};
}
return wrapped;
}
const tools = withLogging({ toolA, toolB, toolC });
const result = await generateText({
model: openai('gpt-4o'),
tools,
prompt: 'Do something',
});
TypeScript: Preserving Types
To maintain type safety when wrapping tools:
import { tool } from 'ai';
import { z } from 'zod';
// If the original tool is typed
type OriginalTool = typeof import('@tpmjs/some-tool').someTool;
// Your wrapper preserves the type
function wrapWithLogging<T extends { execute: (...args: any[]) => any }>(
originalTool: T
): T {
return {
...originalTool,
execute: async (...args: Parameters<T['execute']>) => {
console.log('Calling tool');
return originalTool.execute(...args);
},
} as T;
}
// Usage - types are preserved
import { someTool } from '@tpmjs/some-tool';
const wrappedTool = wrapWithLogging(someTool);
// wrappedTool has the same type as someTool
Real-World Example: Weather Tool with Caching
import { weatherTool } from '@tpmjs/weather';
import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai';
// Simple in-memory cache
const cache = new Map<string, { data: any; expiry: number }>();
const cachedWeatherTool = {
...weatherTool,
execute: async (args: { location: string }, options) => {
const cacheKey = `weather:${args.location.toLowerCase()}`;
const now = Date.now();
// Check cache
const cached = cache.get(cacheKey);
if (cached && cached.expiry > now) {
console.log(`Cache hit for ${args.location}`);
return { ...cached.data, cached: true };
}
// Fetch fresh data
console.log(`Fetching weather for ${args.location}`);
const result = await weatherTool.execute(args, options);
// Cache for 5 minutes
cache.set(cacheKey, {
data: result,
expiry: now + 5 * 60 * 1000,
});
return { ...result, cached: false };
},
};
// Use it
const result = await generateText({
model: openai('gpt-4o'),
tools: { weather: cachedWeatherTool },
prompt: 'What is the weather in San Francisco and New York?',
maxSteps: 3,
});
Summary
| Pattern | Use Case |
|---|---|
| Simple Override | Completely replace execution |
| Wrap Pre/Post | Add logging, metrics |
| Transform Inputs | Modify arguments |
| Transform Outputs | Format results |
| Authentication | Inject API keys |
| Conditional | Mock data, caching |
| Retry | Handle flaky services |
| Timeout | Prevent hanging |
| Rate Limiting | Control API usage |
| Validation | Sanitize inputs |
The key insight is that AI SDK tools are plain objects. You can spread them and override execute to customize behavior while keeping the original description and parameters schema intact.
const customTool = {
...originalTool, // Keep description & parameters
execute: async (args) => { // Override execute
// Your custom logic
},
};