feat: add factory function support to Railway tool executor
Detects and handles tools exported as factory functions that require
configuration before returning the actual AI SDK tool object.
Supports multiple factory patterns:
1. No-args factory: toolName()
2. Config object: toolName({ apiKey: 'xxx' })
3. Single-arg: toolName('api-key-value')
For config objects, tries multiple key name variations:
- Raw env vars: { VALYU_API_KEY: 'xxx' }
- Normalized apiKey: { apiKey: 'xxx' }
- Normalized key: { key: 'xxx' }
This enables dynamic loading of tools like @valyu/ai-sdk paperSearch
that use factory patterns instead of direct tool exports.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
9fc4339d52
commit
9b9f0f0859
1 changed files with 205 additions and 17 deletions
|
|
@ -7,6 +7,7 @@
|
|||
import { zodToJsonSchema } from 'https://esm.sh/zod-to-json-schema@3.25.0';
|
||||
|
||||
// Cache for imported tool modules
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Tool types are dynamic and vary by package
|
||||
const moduleCache = new Map<string, any>();
|
||||
|
||||
/**
|
||||
|
|
@ -15,7 +16,7 @@ const moduleCache = new Map<string, any>();
|
|||
async function loadAndDescribe(req: Request): Promise<Response> {
|
||||
try {
|
||||
const body = await req.json();
|
||||
const { packageName, exportName, version, importUrl } = body;
|
||||
const { packageName, exportName, version, importUrl, env } = body;
|
||||
|
||||
if (!packageName || !exportName || !version) {
|
||||
return Response.json(
|
||||
|
|
@ -29,6 +30,7 @@ async function loadAndDescribe(req: Request): Promise<Response> {
|
|||
|
||||
const cacheKey = `${packageName}::${exportName}`;
|
||||
|
||||
// biome-ignore lint/suspicious/noImplicitAnyLet: Tool type is determined dynamically after import
|
||||
let toolModule;
|
||||
|
||||
// Check cache first
|
||||
|
|
@ -41,9 +43,9 @@ async function loadAndDescribe(req: Request): Promise<Response> {
|
|||
console.log(`📦 Importing: ${url}`);
|
||||
|
||||
const module = await import(url);
|
||||
toolModule = module[exportName];
|
||||
let rawExport = module[exportName];
|
||||
|
||||
if (!toolModule) {
|
||||
if (!rawExport) {
|
||||
console.error(`❌ Export "${exportName}" not found. Available:`, Object.keys(module));
|
||||
return Response.json(
|
||||
{
|
||||
|
|
@ -55,9 +57,97 @@ async function loadAndDescribe(req: Request): Promise<Response> {
|
|||
);
|
||||
}
|
||||
|
||||
// Check if it's a factory function (not a direct tool)
|
||||
if (typeof rawExport === 'function' && !rawExport.description && !rawExport.execute) {
|
||||
console.log(`🏭 Detected factory function for ${cacheKey}, attempting to call...`);
|
||||
|
||||
let factoryResult = null;
|
||||
|
||||
// Strategy 1: Try calling with no arguments
|
||||
try {
|
||||
console.log(` Trying: ${exportName}()`);
|
||||
factoryResult = rawExport();
|
||||
if (factoryResult?.description && factoryResult?.execute) {
|
||||
console.log(' ✅ Success with no-args factory');
|
||||
rawExport = factoryResult;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(' ❌ No-args failed:', error.message);
|
||||
}
|
||||
|
||||
// Strategy 2: Try calling with env vars as config object
|
||||
if (!factoryResult && env && typeof env === 'object') {
|
||||
// Build multiple config variations to try
|
||||
const configVariations = [];
|
||||
|
||||
// Variation 1: Raw env vars (e.g., { VALYU_API_KEY: 'xxx' })
|
||||
configVariations.push({ ...env });
|
||||
|
||||
// Variation 2: Normalized to camelCase apiKey (e.g., { apiKey: 'xxx' })
|
||||
const apiKeyValue = Object.entries(env).find(([key]) =>
|
||||
key.toUpperCase().includes('API_KEY')
|
||||
)?.[1];
|
||||
if (apiKeyValue) {
|
||||
configVariations.push({ apiKey: apiKeyValue });
|
||||
}
|
||||
|
||||
// Variation 3: Normalized to key (e.g., { key: 'xxx' })
|
||||
if (apiKeyValue) {
|
||||
configVariations.push({ key: apiKeyValue });
|
||||
}
|
||||
|
||||
// Try each config variation
|
||||
for (const config of configVariations) {
|
||||
try {
|
||||
console.log(` Trying: ${exportName}(`, Object.keys(config), ')');
|
||||
factoryResult = rawExport(config);
|
||||
if (factoryResult?.description && factoryResult?.execute) {
|
||||
console.log(' ✅ Success with config:', Object.keys(config));
|
||||
rawExport = factoryResult;
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(' ❌ Config', Object.keys(config), 'failed:', error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 3: Try calling with first env var value (single-arg pattern)
|
||||
if (!factoryResult && env && typeof env === 'object') {
|
||||
try {
|
||||
const firstValue = Object.values(env)[0];
|
||||
if (firstValue) {
|
||||
console.log(` Trying: ${exportName}(firstEnvValue)`);
|
||||
factoryResult = rawExport(firstValue);
|
||||
if (factoryResult?.description && factoryResult?.execute) {
|
||||
console.log(' ✅ Success with single-arg factory');
|
||||
rawExport = factoryResult;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(' ❌ Single-arg failed:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// If all factory strategies failed, return error
|
||||
if (!factoryResult) {
|
||||
console.error('❌ Factory function detected but all call strategies failed');
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
error: `Tool "${exportName}" is a factory function but couldn't be initialized. Tried: no-args, config object, and single-arg patterns.`,
|
||||
hint: 'This tool may require specific configuration. Check package documentation.',
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
toolModule = rawExport;
|
||||
|
||||
// Validate it's an AI SDK tool
|
||||
if (!toolModule.description || !toolModule.execute) {
|
||||
console.error(`❌ Invalid AI SDK tool structure:`, {
|
||||
console.error('❌ Invalid AI SDK tool structure:', {
|
||||
hasDescription: !!toolModule.description,
|
||||
hasExecute: !!toolModule.execute,
|
||||
hasInputSchema: !!toolModule.inputSchema,
|
||||
|
|
@ -173,7 +263,7 @@ async function executeTool(req: Request): Promise<Response> {
|
|||
const body = await req.json();
|
||||
const { packageName, exportName, version, importUrl, params, env } = body;
|
||||
|
||||
console.log(`📥 Execute request:`, {
|
||||
console.log('📥 Execute request:', {
|
||||
packageName,
|
||||
exportName,
|
||||
version,
|
||||
|
|
@ -194,6 +284,7 @@ async function executeTool(req: Request): Promise<Response> {
|
|||
const cacheKey = `${packageName}::${exportName}`;
|
||||
const startTime = Date.now();
|
||||
|
||||
// biome-ignore lint/suspicious/noImplicitAnyLet: Tool type is determined dynamically after import
|
||||
let toolModule;
|
||||
|
||||
// Check cache or import
|
||||
|
|
@ -205,19 +296,116 @@ async function executeTool(req: Request): Promise<Response> {
|
|||
console.log(`📦 Importing for execution: ${url}`);
|
||||
|
||||
const module = await import(url);
|
||||
toolModule = module[exportName];
|
||||
let rawExport = module[exportName];
|
||||
|
||||
if (!toolModule || !toolModule.execute) {
|
||||
if (!rawExport) {
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Tool not found or invalid',
|
||||
error: 'Tool not found',
|
||||
executionTimeMs: Date.now() - startTime,
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check if it's a factory function (not a direct tool)
|
||||
if (typeof rawExport === 'function' && !rawExport.description && !rawExport.execute) {
|
||||
console.log(`🏭 Detected factory function for ${cacheKey}, attempting to call...`);
|
||||
|
||||
let factoryResult = null;
|
||||
|
||||
// Strategy 1: Try calling with no arguments
|
||||
try {
|
||||
console.log(` Trying: ${exportName}()`);
|
||||
factoryResult = rawExport();
|
||||
if (factoryResult?.execute) {
|
||||
console.log(' ✅ Success with no-args factory');
|
||||
rawExport = factoryResult;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(' ❌ No-args failed:', error.message);
|
||||
}
|
||||
|
||||
// Strategy 2: Try calling with env vars as config object
|
||||
if (!factoryResult && env && typeof env === 'object') {
|
||||
// Build multiple config variations to try
|
||||
const configVariations = [];
|
||||
|
||||
// Variation 1: Raw env vars (e.g., { VALYU_API_KEY: 'xxx' })
|
||||
configVariations.push({ ...env });
|
||||
|
||||
// Variation 2: Normalized to camelCase apiKey (e.g., { apiKey: 'xxx' })
|
||||
const apiKeyValue = Object.entries(env).find(([key]) =>
|
||||
key.toUpperCase().includes('API_KEY')
|
||||
)?.[1];
|
||||
if (apiKeyValue) {
|
||||
configVariations.push({ apiKey: apiKeyValue });
|
||||
}
|
||||
|
||||
// Variation 3: Normalized to key (e.g., { key: 'xxx' })
|
||||
if (apiKeyValue) {
|
||||
configVariations.push({ key: apiKeyValue });
|
||||
}
|
||||
|
||||
// Try each config variation
|
||||
for (const config of configVariations) {
|
||||
try {
|
||||
console.log(` Trying: ${exportName}(`, Object.keys(config), ')');
|
||||
factoryResult = rawExport(config);
|
||||
if (factoryResult?.execute) {
|
||||
console.log(' ✅ Success with config:', Object.keys(config));
|
||||
rawExport = factoryResult;
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(' ❌ Config', Object.keys(config), 'failed:', error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 3: Try calling with first env var value (single-arg pattern)
|
||||
if (!factoryResult && env && typeof env === 'object') {
|
||||
try {
|
||||
const firstValue = Object.values(env)[0];
|
||||
if (firstValue) {
|
||||
console.log(` Trying: ${exportName}(firstEnvValue)`);
|
||||
factoryResult = rawExport(firstValue);
|
||||
if (factoryResult?.execute) {
|
||||
console.log(' ✅ Success with single-arg factory');
|
||||
rawExport = factoryResult;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(' ❌ Single-arg failed:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (!factoryResult) {
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
error: `Tool "${exportName}" is a factory function but couldn't be initialized`,
|
||||
executionTimeMs: Date.now() - startTime,
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
toolModule = rawExport;
|
||||
|
||||
if (!toolModule.execute) {
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Tool missing execute function',
|
||||
executionTimeMs: Date.now() - startTime,
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
moduleCache.set(cacheKey, toolModule);
|
||||
}
|
||||
|
||||
|
|
@ -232,14 +420,14 @@ async function executeTool(req: Request): Promise<Response> {
|
|||
}
|
||||
// Verify they're set
|
||||
console.log(
|
||||
`🔍 Verification - Deno.env has:`,
|
||||
'🔍 Verification - Deno.env has:',
|
||||
envKeys.map((k) => `${k}=${Deno.env.get(k)?.substring(0, 10)}...`)
|
||||
);
|
||||
} else {
|
||||
console.log(`⚠️ No env vars provided in request`);
|
||||
console.log('⚠️ No env vars provided in request');
|
||||
}
|
||||
} else {
|
||||
console.log(`⚠️ No env object in request body`);
|
||||
console.log('⚠️ No env object in request body');
|
||||
}
|
||||
|
||||
// Execute the tool
|
||||
|
|
@ -366,12 +554,12 @@ async function handler(req: Request): Promise<Response> {
|
|||
const port = Number.parseInt(Deno.env.get('PORT') || '3002');
|
||||
|
||||
console.log(`🚀 Railway Tool Executor (Deno) running on port ${port}`);
|
||||
console.log(`📦 HTTP imports: ENABLED`);
|
||||
console.log('📦 HTTP imports: ENABLED');
|
||||
console.log(`🔗 Health check: http://localhost:${port}/health`);
|
||||
console.log(`🛠️ Endpoints:`);
|
||||
console.log(` POST /load-and-describe - Load tool and get schema`);
|
||||
console.log(` POST /execute-tool - Execute a tool with params`);
|
||||
console.log(` POST /cache/clear - Clear module cache`);
|
||||
console.log(` GET /cache/stats - Get cache statistics`);
|
||||
console.log('🛠️ Endpoints:');
|
||||
console.log(' POST /load-and-describe - Load tool and get schema');
|
||||
console.log(' POST /execute-tool - Execute a tool with params');
|
||||
console.log(' POST /cache/clear - Clear module cache');
|
||||
console.log(' GET /cache/stats - Get cache statistics');
|
||||
|
||||
Deno.serve({ port }, handler);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue