tpmjs/apps/web/src/app/api/tools/execute/[...slug]/route.ts
Ajax Davis 38cc823a58 fix: mark AI tool execution route as dynamic to prevent build-time WASM error
Add `export const dynamic = 'force-dynamic'` to `/api/tools/execute/[...slug]`
to prevent Next.js from attempting static generation at build time.

The AI SDK (used via executeToolWithAgent) requires tiktoken_bg.wasm which
cannot be loaded during static generation. Marking as dynamic ensures the
route is only executed at runtime.

Note: This partially addresses the build error but further investigation needed
for complete resolution of tiktoken WASM loading in Next.js 16 + Turbopack.

Relates to: "Error: Missing tiktoken_bg.wasm"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 01:42:04 +10:00

191 lines
5.7 KiB
TypeScript

/**
* Tool execution endpoint with SSE streaming
* Executes TPMJS tools with AI agents and streams real-time progress
*/
import { executeToolWithAgent } from '@/lib/ai-agent/tool-executor-agent';
import { checkRateLimit, getClientIP } from '@/lib/rate-limiter';
import { prisma } from '@tpmjs/db';
import { type NextRequest, NextResponse } from 'next/server';
// Use Node.js runtime for SSE streaming
export const runtime = 'nodejs';
export const maxDuration = 60; // 60 seconds timeout
export const dynamic = 'force-dynamic'; // Prevent static generation for AI SDK routes
interface ExecuteRequest {
prompt: string;
parameters?: Record<string, unknown>;
}
/**
* POST /api/tools/[...slug]/execute
* Executes a tool with an AI agent and streams the response via SSE
*/
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ slug: string[] }> }
) {
const { slug } = await params;
const packageName = decodeURIComponent(slug.join('/'));
try {
// Parse request body
const body = (await request.json()) as ExecuteRequest;
const { prompt, parameters } = body;
if (!prompt || prompt.length === 0) {
return NextResponse.json({ error: 'Prompt is required' }, { status: 400 });
}
if (prompt.length > 2000) {
return NextResponse.json({ error: 'Prompt too long (max 2000 characters)' }, { status: 400 });
}
// Get client IP and check rate limit
const ipAddress = getClientIP(request);
const rateLimit = await checkRateLimit(ipAddress);
if (!rateLimit.allowed) {
return NextResponse.json(
{
error: 'Rate limit exceeded',
resetAt: rateLimit.resetAt,
remaining: 0,
},
{
status: 429,
headers: {
'X-RateLimit-Limit': '10',
'X-RateLimit-Remaining': '0',
'X-RateLimit-Reset': rateLimit.resetAt.toISOString(),
},
}
);
}
// Fetch tool from database
const tool = await prisma.tool.findUnique({
where: { npmPackageName: packageName },
});
if (!tool) {
return NextResponse.json({ error: 'Tool not found' }, { status: 404 });
}
// Create simulation record
const simulation = await prisma.simulation.create({
data: {
toolId: tool.id,
userPrompt: prompt,
// biome-ignore lint/suspicious/noExplicitAny: Prisma Json type compatibility workaround
parameters: parameters ? (parameters as any) : undefined,
ipAddress,
userAgent: request.headers.get('user-agent') || null,
status: 'running',
model: 'gpt-4-turbo',
},
});
// Create readable stream for SSE
const stream = new ReadableStream({
async start(controller) {
const encoder = new TextEncoder();
const sendEvent = (event: string, data: unknown) => {
const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
controller.enqueue(encoder.encode(message));
};
try {
const startTime = Date.now();
// Execute tool with AI agent
const result = await executeToolWithAgent(
tool,
prompt,
(chunk) => {
// Stream text chunks
sendEvent('chunk', { text: chunk });
},
(tokens) => {
// Stream token updates
sendEvent('tokens', tokens);
}
);
const executionTimeMs = Date.now() - startTime;
// Update simulation with results
await prisma.simulation.update({
where: { id: simulation.id },
data: {
status: 'success',
output: { result: result.output },
agentSteps: result.agentSteps,
executionTimeMs,
completedAt: new Date(),
},
});
// Create token usage record
await prisma.tokenUsage.create({
data: {
simulationId: simulation.id,
inputTokens: result.tokenBreakdown.inputTokens,
toolDescTokens: result.tokenBreakdown.toolDescTokens,
schemaTokens: result.tokenBreakdown.schemaTokens,
outputTokens: result.tokenBreakdown.outputTokens,
totalTokens: result.tokenBreakdown.totalTokens,
estimatedCost: result.tokenBreakdown.estimatedCost,
},
});
// Send completion event
sendEvent('complete', {
output: result.output,
tokenBreakdown: result.tokenBreakdown,
executionTimeMs,
agentSteps: result.agentSteps,
});
} catch (error) {
// Update simulation with error
await prisma.simulation.update({
where: { id: simulation.id },
data: {
status: 'error',
error: error instanceof Error ? error.message : 'Unknown error',
completedAt: new Date(),
},
});
// Send error event
sendEvent('error', {
message: error instanceof Error ? error.message : 'Unknown error',
});
} finally {
controller.close();
}
},
});
// Return SSE stream
return new NextResponse(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
'X-RateLimit-Limit': '10',
'X-RateLimit-Remaining': rateLimit.remaining.toString(),
},
});
} catch (error) {
console.error('Execute endpoint error:', error);
return NextResponse.json(
{
error: error instanceof Error ? error.message : 'Internal server error',
},
{ status: 500 }
);
}
}