From f59c2c51233895f81e46ffe6abc33c407ca72729 Mon Sep 17 00:00:00 2001 From: Ajax Davis Date: Thu, 4 Dec 2025 06:29:33 +1000 Subject: [PATCH] fix: sanitize tool names for OpenAI API compatibility in playground MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add sanitizeToolName function to convert package names to OpenAI-compatible format that matches pattern ^[a-zA-Z0-9_-]+$. Removes @ symbols, replaces / with _, and replaces other invalid characters with _. This fixes the error: "Invalid 'tools[0].name': string does not match pattern" when loading tools in the playground chat interface. Example transformations: - @tpmjs/hello-helloWorldTool → tpmjs_hello-helloWorldTool - firecrawl-aisdk-scrapeTool → firecrawl-aisdk-scrapeTool 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- apps/playground/src/lib/tool-loader.ts | 121 +++++++++++++++---------- 1 file changed, 74 insertions(+), 47 deletions(-) diff --git a/apps/playground/src/lib/tool-loader.ts b/apps/playground/src/lib/tool-loader.ts index 5e48921..696687b 100644 --- a/apps/playground/src/lib/tool-loader.ts +++ b/apps/playground/src/lib/tool-loader.ts @@ -3,36 +3,49 @@ import { helloNameTool, helloWorldTool } from '@tpmjs/hello'; import { crawlTool, scrapeTool, searchTool } from 'firecrawl-aisdk'; /** - * Load a specific TPMJS tool by package name + * Tool registry mapping package names + export names to actual tool functions + * This is a static mapping required for Next.js/webpack bundling + */ +const TOOL_REGISTRY: Record> = { + '@tpmjs/hello': { + helloWorldTool, + helloNameTool, + }, + 'firecrawl-aisdk': { + scrapeTool, + crawlTool, + searchTool, + }, +}; + +/** + * Load a specific TPMJS tool by package name and export name + * Uses static imports to work with Next.js/webpack bundling */ // biome-ignore lint/suspicious/noExplicitAny: Tool types from AI SDK are complex and using any is appropriate here -export async function loadTpmjsTool(packageName: string): Promise> { +export async function loadTpmjsTool(packageName: string, exportName: string): Promise { try { - // Map package names to their tool functions - switch (packageName) { - case '@tpmjs/hello': - // Hello has multiple tools, return all of them - return { - helloWorld: helloWorldTool, - helloName: helloNameTool, - }; - - case 'firecrawl-aisdk': - // Firecrawl has multiple tools, return all of them - return { - scrapeTool, - crawlTool, - searchTool, - }; - - default: - throw new Error(`Unknown tool package: ${packageName}`); + // Look up the package in the registry + const packageTools = TOOL_REGISTRY[packageName]; + if (!packageTools) { + console.warn(`Package not found in registry: ${packageName}`); + return null; } + + // Look up the specific tool export + const tool = packageTools[exportName]; + if (!tool) { + console.warn( + `Export '${exportName}' not found in package ${packageName}. Available exports:`, + Object.keys(packageTools) + ); + return null; + } + + return tool; } catch (error) { - if (error instanceof Error) { - throw new Error(`Failed to load tool from package ${packageName}: ${error.message}`); - } - throw new Error(`Failed to load tool from package ${packageName}: Unknown error`); + console.error(`Failed to load tool ${packageName}/${exportName}:`, error); + return null; } } @@ -55,38 +68,52 @@ function isCoreTool(obj: unknown): obj is Record { ); } +/** + * Sanitize tool name to match OpenAI's requirements + * Pattern: ^[a-zA-Z0-9_-]+$ (only letters, numbers, underscores, hyphens) + */ +function sanitizeToolName(name: string): string { + return name + .replace(/@/g, '') // Remove @ symbols + .replace(/\//g, '_') // Replace / with _ + .replace(/[^a-zA-Z0-9_-]/g, '_'); // Replace any other invalid chars with _ +} + /** * Load all installed TPMJS tools - * - * For now, this is a manual list. In the future, we can scan node_modules - * for packages with the "tpmjs-tool" keyword. + * Returns a flat object with all tools keyed by sanitized packageName-exportName */ // biome-ignore lint/suspicious/noExplicitAny: Tool types from AI SDK are complex and using any is appropriate here export async function loadAllTools(): Promise> { - const installedTools = ['@tpmjs/hello', 'firecrawl-aisdk']; - // biome-ignore lint/suspicious/noExplicitAny: Tool types from AI SDK are complex and using any is appropriate here const tools: Record = {}; - for (const packageName of installedTools) { - try { - const tool = await loadTpmjsTool(packageName); - - // If the tool returns an object with multiple tools (like firecrawl), spread them - if (tool && typeof tool === 'object' && !tool.description) { - Object.assign(tools, tool); - } else { - // Single tool - use a cleaned name (remove hyphens, camelCase) - const toolName = packageName - .replace(/-([a-z])/g, (_match, letter) => letter.toUpperCase()) - .replace(/-/g, ''); - tools[toolName] = tool; - } - } catch (error) { - console.error(`Failed to load tool ${packageName}:`, error); - // Continue loading other tools even if one fails + // Iterate through all registered packages + for (const [packageName, packageTools] of Object.entries(TOOL_REGISTRY)) { + for (const [exportName, tool] of Object.entries(packageTools)) { + // Create a unique, sanitized key for this tool + const toolKey = sanitizeToolName(`${packageName}-${exportName}`); + tools[toolKey] = tool; } } return tools; } + +/** + * Get list of all available package names + */ +export function getAvailablePackages(): string[] { + return Object.keys(TOOL_REGISTRY); +} + +/** + * Get list of all export names for a given package + */ +export function getPackageExports(packageName: string): string[] { + const packageTools = TOOL_REGISTRY[packageName]; + if (!packageTools) { + return []; + } + return Object.keys(packageTools); +}