Back to Blog

Vercel AI SDK: Streaming Code Execution with HopX

TutorialsAlin Dobra15 min read

Vercel AI SDK: Streaming Code Execution with HopX

The Vercel AI SDK makes building AI applications delightful. Streaming responses, tool calling, and React hooks that "just work." But when your AI needs to execute code, you hit a wall: where do you run it safely?

This tutorial shows how to integrate HopX sandboxes with the Vercel AI SDK for real-time, streaming code execution. Your users see output as it happens, character by character.

What We're Building

text
1
2
                     User Chat Interface                          
3
                                                                  
4
  User: "Calculate the factorial of 100"                         
5
                                                                  
6
  Assistant: I'll calculate that for you...                      
7
                                                                  
8
       
9
   >>> Executing Python...                                       
10
   factorial(100) = 933262154439441526816992388...                Streaming
11
    Completed in 0.3s                                          
12
       
13
                                                                  
14
  The factorial of 100 is a 158-digit number: 9.33×10^157       
15
16
 

Prerequisites

bash
1
npm install ai @ai-sdk/openai @hopx-ai/sdk
2
 

Set environment variables:

bash
1
OPENAI_API_KEY=sk-...
2
HOPX_API_KEY=...
3
 

Project Structure

text
1
app/
2
 api/
3
    chat/
4
        route.ts      # AI chat endpoint with tool calling
5
 components/
6
    chat.tsx          # Chat UI component
7
    code-output.tsx   # Streaming code output display
8
 page.tsx              # Main page
9
 

Step 1: Create the Chat API Route

The API route handles chat messages and tool execution:

typescript
1
// app/api/chat/route.ts
2
import { openai } from '@ai-sdk/openai';
3
import { streamText, tool } from 'ai';
4
import { z } from 'zod';
5
import { Sandbox } from '@hopx-ai/sdk';
6
 
7
// Allow streaming responses up to 60 seconds
8
export const maxDuration = 60;
9
 
10
export async function POST(req: Request) {
11
  const { messages } = await req.json();
12
 
13
  const result = streamText({
14
    model: openai('gpt-4o'),
15
    system: `You are a helpful AI assistant that can execute Python code.
16
 
17
When users ask you to calculate, analyze data, or do anything that requires computation:
18
1. Write Python code to accomplish the task
19
2. Use the execute_python tool to run it
20
3. Explain the results clearly
21
 
22
The sandbox has pandas, numpy, matplotlib, and standard libraries.
23
For charts, save to /app/output.png.`,
24
    
25
    messages,
26
    
27
    tools: {
28
      execute_python: tool({
29
        description: 'Execute Python code in a secure sandbox. Use for calculations, data analysis, and any computational task.',
30
        parameters: z.object({
31
          code: z.string().describe('Python code to execute'),
32
          description: z.string().describe('Brief description of what this code does'),
33
        }),
34
        execute: async ({ code, description }) => {
35
          const sandbox = await Sandbox.create({
36
            template: 'code-interpreter',
37
            apiKey: process.env.HOPX_API_KEY,
38
          });
39
          
40
          try {
41
            const result = await sandbox.runCode(code, {
42
              language: 'python',
43
              timeout: 30,
44
            });
45
            
46
            return {
47
              success: result.exitCode === 0,
48
              output: result.stdout || '',
49
              error: result.stderr || '',
50
              exitCode: result.exitCode,
51
              description,
52
            };
53
          } finally {
54
            await sandbox.kill();
55
          }
56
        },
57
      }),
58
    },
59
    
60
    // Maximum tool invocations per message
61
    maxSteps: 5,
62
  });
63
 
64
  return result.toDataStreamResponse();
65
}
66
 

Step 2: Create the Chat Component

A React component using the useChat hook:

tsx
1
// app/components/chat.tsx
2
'use client';
3
 
4
import { useChat } from 'ai/react';
5
import { CodeOutput } from './code-output';
6
 
7
export function Chat() {
8
  const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
9
    api: '/api/chat',
10
  });
11
 
12
  return (
13
    <div className="flex flex-col h-screen max-w-3xl mx-auto p-4">
14
      {/* Messages */}
15
      <div className="flex-1 overflow-y-auto space-y-4 mb-4">
16
        {messages.map((message) => (
17
          <div
18
            key={message.id}
19
            className={`flex ${
20
              message.role === 'user' ? 'justify-end' : 'justify-start'
21
            }`}
22
          >
23
            <div
24
              className={`max-w-[80%] rounded-lg px-4 py-2 ${
25
                message.role === 'user'
26
                  ? 'bg-blue-600 text-white'
27
                  : 'bg-gray-100 text-gray-900'
28
              }`}
29
            >
30
              {/* Render text content */}
31
              <p className="whitespace-pre-wrap">{message.content}</p>
32
              
33
              {/* Render tool invocations */}
34
              {message.toolInvocations?.map((invocation) => (
35
                <CodeOutput
36
                  key={invocation.toolCallId}
37
                  invocation={invocation}
38
                />
39
              ))}
40
            </div>
41
          </div>
42
        ))}
43
        
44
        {isLoading && (
45
          <div className="flex justify-start">
46
            <div className="bg-gray-100 rounded-lg px-4 py-2">
47
              <span className="animate-pulse">Thinking...</span>
48
            </div>
49
          </div>
50
        )}
51
      </div>
52
 
53
      {/* Input form */}
54
      <form onSubmit={handleSubmit} className="flex gap-2">
55
        <input
56
          value={input}
57
          onChange={handleInputChange}
58
          placeholder="Ask me to calculate something..."
59
          className="flex-1 px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
60
          disabled={isLoading}
61
        />
62
        <button
63
          type="submit"
64
          disabled={isLoading || !input.trim()}
65
          className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
66
        >
67
          Send
68
        </button>
69
      </form>
70
    </div>
71
  );
72
}
73
 

Step 3: Create the Code Output Component

Display code execution results with syntax highlighting:

tsx
1
// app/components/code-output.tsx
2
'use client';
3
 
4
import { ToolInvocation } from 'ai';
5
 
6
interface CodeOutputProps {
7
  invocation: ToolInvocation;
8
}
9
 
10
export function CodeOutput({ invocation }: CodeOutputProps) {
11
  // Extract tool result if available
12
  const result = 'result' in invocation ? invocation.result : null;
13
  const args = invocation.args as { code: string; description: string };
14
  
15
  const isComplete = 'result' in invocation;
16
  const success = result?.success;
17
  
18
  return (
19
    <div className="mt-3 rounded-lg border border-gray-200 overflow-hidden">
20
      {/* Header */}
21
      <div className="flex items-center justify-between px-3 py-2 bg-gray-50 border-b border-gray-200">
22
        <div className="flex items-center gap-2">
23
          <span className="text-xs font-medium text-gray-500">Python</span>
24
          <span className="text-xs text-gray-400">{args.description}</span>
25
        </div>
26
        <div className="flex items-center gap-2">
27
          {!isComplete && (
28
            <span className="flex items-center gap-1 text-xs text-amber-600">
29
              <span className="w-2 h-2 bg-amber-500 rounded-full animate-pulse" />
30
              Executing...
31
            </span>
32
          )}
33
          {isComplete && success && (
34
            <span className="flex items-center gap-1 text-xs text-green-600">
35
              <span className="w-2 h-2 bg-green-500 rounded-full" />
36
              Success
37
            </span>
38
          )}
39
          {isComplete && !success && (
40
            <span className="flex items-center gap-1 text-xs text-red-600">
41
              <span className="w-2 h-2 bg-red-500 rounded-full" />
42
              Error
43
            </span>
44
          )}
45
        </div>
46
      </div>
47
      
48
      {/* Code */}
49
      <div className="bg-gray-900 p-3">
50
        <pre className="text-sm text-gray-100 overflow-x-auto">
51
          <code>{args.code}</code>
52
        </pre>
53
      </div>
54
      
55
      {/* Output */}
56
      {isComplete && (
57
        <div className="border-t border-gray-200">
58
          <div className="px-3 py-1 bg-gray-50 border-b border-gray-200">
59
            <span className="text-xs font-medium text-gray-500">Output</span>
60
          </div>
61
          <div className="p-3 bg-black">
62
            {result.output && (
63
              <pre className="text-sm text-green-400 whitespace-pre-wrap">
64
                {result.output}
65
              </pre>
66
            )}
67
            {result.error && (
68
              <pre className="text-sm text-red-400 whitespace-pre-wrap">
69
                {result.error}
70
              </pre>
71
            )}
72
            {!result.output && !result.error && (
73
              <span className="text-sm text-gray-500 italic">No output</span>
74
            )}
75
          </div>
76
        </div>
77
      )}
78
    </div>
79
  );
80
}
81
 

Step 4: Wire It Up

Create the main page:

tsx
1
// app/page.tsx
2
import { Chat } from './components/chat';
3
 
4
export default function Home() {
5
  return (
6
    <main className="min-h-screen bg-white">
7
      <Chat />
8
    </main>
9
  );
10
}
11
 

Advanced: True Streaming Output

The basic implementation waits for code to finish before showing output. For real-time streaming, we need a custom approach:

Streaming API Route

typescript
1
// app/api/execute/route.ts
2
import { NextRequest } from 'next/server';
3
import { Sandbox } from '@hopx-ai/sdk';
4
 
5
export async function POST(req: NextRequest) {
6
  const { code, language = 'python' } = await req.json();
7
 
8
  const encoder = new TextEncoder();
9
  
10
  const stream = new ReadableStream({
11
    async start(controller) {
12
      const sandbox = await Sandbox.create({
13
        template: 'code-interpreter',
14
        apiKey: process.env.HOPX_API_KEY,
15
      });
16
 
17
      try {
18
        // Send status
19
        controller.enqueue(
20
          encoder.encode(`data: ${JSON.stringify({ type: 'status', message: 'Executing...' })}\n\n`)
21
        );
22
 
23
        const result = await sandbox.runCode(code, {
24
          language,
25
          timeout: 60,
26
        });
27
 
28
        // Send output
29
        if (result.stdout) {
30
          controller.enqueue(
31
            encoder.encode(`data: ${JSON.stringify({ type: 'stdout', text: result.stdout })}\n\n`)
32
          );
33
        }
34
 
35
        if (result.stderr) {
36
          controller.enqueue(
37
            encoder.encode(`data: ${JSON.stringify({ type: 'stderr', text: result.stderr })}\n\n`)
38
          );
39
        }
40
 
41
        // Send completion
42
        controller.enqueue(
43
          encoder.encode(`data: ${JSON.stringify({ 
44
            type: 'done', 
45
            exitCode: result.exitCode,
46
            success: result.exitCode === 0 
47
          })}\n\n`)
48
        );
49
 
50
      } catch (error) {
51
        controller.enqueue(
52
          encoder.encode(`data: ${JSON.stringify({ 
53
            type: 'error', 
54
            message: error instanceof Error ? error.message : 'Execution failed' 
55
          })}\n\n`)
56
        );
57
      } finally {
58
        await sandbox.kill();
59
        controller.close();
60
      }
61
    },
62
  });
63
 
64
  return new Response(stream, {
65
    headers: {
66
      'Content-Type': 'text/event-stream',
67
      'Cache-Control': 'no-cache',
68
      'Connection': 'keep-alive',
69
    },
70
  });
71
}
72
 

Streaming Hook

tsx
1
// hooks/use-code-execution.ts
2
'use client';
3
 
4
import { useState, useCallback } from 'react';
5
 
6
interface ExecutionState {
7
  status: 'idle' | 'running' | 'done' | 'error';
8
  stdout: string;
9
  stderr: string;
10
  exitCode: number | null;
11
  error: string | null;
12
}
13
 
14
export function useCodeExecution() {
15
  const [state, setState] = useState<ExecutionState>({
16
    status: 'idle',
17
    stdout: '',
18
    stderr: '',
19
    exitCode: null,
20
    error: null,
21
  });
22
 
23
  const execute = useCallback(async (code: string, language = 'python') => {
24
    setState({
25
      status: 'running',
26
      stdout: '',
27
      stderr: '',
28
      exitCode: null,
29
      error: null,
30
    });
31
 
32
    try {
33
      const response = await fetch('/api/execute', {
34
        method: 'POST',
35
        headers: { 'Content-Type': 'application/json' },
36
        body: JSON.stringify({ code, language }),
37
      });
38
 
39
      const reader = response.body?.getReader();
40
      const decoder = new TextDecoder();
41
 
42
      if (!reader) {
43
        throw new Error('No response body');
44
      }
45
 
46
      let buffer = '';
47
 
48
      while (true) {
49
        const { done, value } = await reader.read();
50
        if (done) break;
51
 
52
        buffer += decoder.decode(value, { stream: true });
53
        const lines = buffer.split('\n');
54
        buffer = lines.pop() || '';
55
 
56
        for (const line of lines) {
57
          if (line.startsWith('data: ')) {
58
            try {
59
              const data = JSON.parse(line.slice(6));
60
 
61
              switch (data.type) {
62
                case 'status':
63
                  // Update status message if needed
64
                  break;
65
 
66
                case 'stdout':
67
                  setState((prev) => ({
68
                    ...prev,
69
                    stdout: prev.stdout + data.text,
70
                  }));
71
                  break;
72
 
73
                case 'stderr':
74
                  setState((prev) => ({
75
                    ...prev,
76
                    stderr: prev.stderr + data.text,
77
                  }));
78
                  break;
79
 
80
                case 'done':
81
                  setState((prev) => ({
82
                    ...prev,
83
                    status: 'done',
84
                    exitCode: data.exitCode,
85
                  }));
86
                  break;
87
 
88
                case 'error':
89
                  setState((prev) => ({
90
                    ...prev,
91
                    status: 'error',
92
                    error: data.message,
93
                  }));
94
                  break;
95
              }
96
            } catch {
97
              // Ignore parse errors
98
            }
99
          }
100
        }
101
      }
102
    } catch (error) {
103
      setState((prev) => ({
104
        ...prev,
105
        status: 'error',
106
        error: error instanceof Error ? error.message : 'Unknown error',
107
      }));
108
    }
109
  }, []);
110
 
111
  const reset = useCallback(() => {
112
    setState({
113
      status: 'idle',
114
      stdout: '',
115
      stderr: '',
116
      exitCode: null,
117
      error: null,
118
    });
119
  }, []);
120
 
121
  return { ...state, execute, reset };
122
}
123
 

Streaming Code Block Component

tsx
1
// components/streaming-code-block.tsx
2
'use client';
3
 
4
import { useState } from 'react';
5
import { useCodeExecution } from '@/hooks/use-code-execution';
6
import { Play, Loader2, RotateCcw } from 'lucide-react';
7
 
8
interface StreamingCodeBlockProps {
9
  code: string;
10
  language?: string;
11
}
12
 
13
export function StreamingCodeBlock({ code, language = 'python' }: StreamingCodeBlockProps) {
14
  const { status, stdout, stderr, exitCode, error, execute, reset } = useCodeExecution();
15
  const [showOutput, setShowOutput] = useState(false);
16
 
17
  const handleRun = async () => {
18
    setShowOutput(true);
19
    await execute(code, language);
20
  };
21
 
22
  return (
23
    <div className="rounded-lg border border-gray-200 overflow-hidden">
24
      {/* Code header */}
25
      <div className="flex items-center justify-between px-4 py-2 bg-gray-50 border-b">
26
        <span className="text-sm font-mono text-gray-600">{language}</span>
27
        <div className="flex items-center gap-2">
28
          {status === 'idle' && (
29
            <button
30
              onClick={handleRun}
31
              className="flex items-center gap-1 px-3 py-1 text-sm bg-green-600 text-white rounded hover:bg-green-700"
32
            >
33
              <Play className="w-4 h-4" />
34
              Run
35
            </button>
36
          )}
37
          {status === 'running' && (
38
            <span className="flex items-center gap-1 text-sm text-amber-600">
39
              <Loader2 className="w-4 h-4 animate-spin" />
40
              Running...
41
            </span>
42
          )}
43
          {(status === 'done' || status === 'error') && (
44
            <button
45
              onClick={() => {
46
                reset();
47
                setShowOutput(false);
48
              }}
49
              className="flex items-center gap-1 px-3 py-1 text-sm bg-gray-600 text-white rounded hover:bg-gray-700"
50
            >
51
              <RotateCcw className="w-4 h-4" />
52
              Reset
53
            </button>
54
          )}
55
        </div>
56
      </div>
57
 
58
      {/* Code content */}
59
      <pre className="p-4 bg-gray-900 text-gray-100 overflow-x-auto">
60
        <code>{code}</code>
61
      </pre>
62
 
63
      {/* Output panel */}
64
      {showOutput && (
65
        <div className="border-t border-gray-200">
66
          <div className="px-4 py-2 bg-gray-50 border-b flex items-center justify-between">
67
            <span className="text-sm font-medium text-gray-600">Output</span>
68
            {status === 'done' && exitCode !== null && (
69
              <span className={`text-xs ${exitCode === 0 ? 'text-green-600' : 'text-red-600'}`}>
70
                Exit code: {exitCode}
71
              </span>
72
            )}
73
          </div>
74
          <div className="p-4 bg-black min-h-[100px] max-h-[300px] overflow-auto font-mono text-sm">
75
            {stdout && <pre className="text-green-400 whitespace-pre-wrap">{stdout}</pre>}
76
            {stderr && <pre className="text-red-400 whitespace-pre-wrap">{stderr}</pre>}
77
            {error && <pre className="text-red-400">{error}</pre>}
78
            {status === 'running' && !stdout && !stderr && (
79
              <span className="text-gray-500 animate-pulse">Waiting for output...</span>
80
            )}
81
          </div>
82
        </div>
83
      )}
84
    </div>
85
  );
86
}
87
 

Integration with AI SDK Tools

Combine the streaming execution with AI SDK tool calling:

typescript
1
// app/api/chat/route.ts
2
import { openai } from '@ai-sdk/openai';
3
import { streamText, tool } from 'ai';
4
import { z } from 'zod';
5
import { Sandbox } from '@hopx-ai/sdk';
6
 
7
// Persistent sandbox for conversation
8
let sandbox: Sandbox | null = null;
9
 
10
async function getOrCreateSandbox() {
11
  if (!sandbox) {
12
    sandbox = await Sandbox.create({
13
      template: 'code-interpreter',
14
      apiKey: process.env.HOPX_API_KEY,
15
      ttl: 300, // 5 minutes
16
    });
17
  }
18
  return sandbox;
19
}
20
 
21
export async function POST(req: Request) {
22
  const { messages } = await req.json();
23
 
24
  const result = streamText({
25
    model: openai('gpt-4o'),
26
    messages,
27
    
28
    tools: {
29
      execute_python: tool({
30
        description: 'Execute Python code. State persists between calls.',
31
        parameters: z.object({
32
          code: z.string(),
33
        }),
34
        execute: async ({ code }) => {
35
          const sb = await getOrCreateSandbox();
36
          
37
          const result = await sb.runCode(code, {
38
            language: 'python',
39
            timeout: 30,
40
          });
41
          
42
          return {
43
            output: result.stdout,
44
            error: result.stderr,
45
            success: result.exitCode === 0,
46
          };
47
        },
48
      }),
49
      
50
      install_package: tool({
51
        description: 'Install a Python package using pip',
52
        parameters: z.object({
53
          package: z.string().describe('Package name to install'),
54
        }),
55
        execute: async ({ package: pkg }) => {
56
          const sb = await getOrCreateSandbox();
57
          
58
          const result = await sb.runCode(`pip install ${pkg}`, {
59
            language: 'bash',
60
            timeout: 60,
61
          });
62
          
63
          return {
64
            success: result.exitCode === 0,
65
            output: result.stdout,
66
            error: result.stderr,
67
          };
68
        },
69
      }),
70
      
71
      read_file: tool({
72
        description: 'Read a file from the sandbox',
73
        parameters: z.object({
74
          path: z.string().describe('File path to read'),
75
        }),
76
        execute: async ({ path }) => {
77
          const sb = await getOrCreateSandbox();
78
          
79
          try {
80
            const content = await sb.files.read(path);
81
            return { success: true, content };
82
          } catch (e) {
83
            return { 
84
              success: false, 
85
              error: e instanceof Error ? e.message : 'File not found' 
86
            };
87
          }
88
        },
89
      }),
90
      
91
      write_file: tool({
92
        description: 'Write content to a file in the sandbox',
93
        parameters: z.object({
94
          path: z.string().describe('File path to write'),
95
          content: z.string().describe('Content to write'),
96
        }),
97
        execute: async ({ path, content }) => {
98
          const sb = await getOrCreateSandbox();
99
          
100
          try {
101
            await sb.files.write(path, content);
102
            return { success: true, path };
103
          } catch (e) {
104
            return { 
105
              success: false, 
106
              error: e instanceof Error ? e.message : 'Write failed' 
107
            };
108
          }
109
        },
110
      }),
111
    },
112
    
113
    maxSteps: 10,
114
  });
115
 
116
  return result.toDataStreamResponse();
117
}
118
 

Multi-Language Support

Extend the tools to support multiple languages:

typescript
1
import { z } from 'zod';
2
 
3
const executeCode = tool({
4
  description: 'Execute code in Python, JavaScript, TypeScript, or Bash',
5
  parameters: z.object({
6
    code: z.string().describe('Code to execute'),
7
    language: z.enum(['python', 'javascript', 'typescript', 'bash'])
8
      .describe('Programming language'),
9
  }),
10
  execute: async ({ code, language }) => {
11
    const sandbox = await Sandbox.create({
12
      template: 'code-interpreter',
13
      apiKey: process.env.HOPX_API_KEY,
14
    });
15
    
16
    try {
17
      const result = await sandbox.runCode(code, {
18
        language,
19
        timeout: 30,
20
      });
21
      
22
      return {
23
        language,
24
        output: result.stdout,
25
        error: result.stderr,
26
        success: result.exitCode === 0,
27
      };
28
    } finally {
29
      await sandbox.kill();
30
    }
31
  },
32
});
33
 

Error Handling Best Practices

typescript
1
// Wrap tool execution with error handling
2
const safeExecute = async (fn: () => Promise<any>) => {
3
  try {
4
    return await fn();
5
  } catch (error) {
6
    if (error instanceof Error) {
7
      // Check for specific error types
8
      if (error.message.includes('timeout')) {
9
        return {
10
          success: false,
11
          error: 'Code execution timed out. Try simplifying your code.',
12
        };
13
      }
14
      if (error.message.includes('memory')) {
15
        return {
16
          success: false,
17
          error: 'Out of memory. Try processing smaller data chunks.',
18
        };
19
      }
20
    }
21
    return {
22
      success: false,
23
      error: 'Execution failed. Please try again.',
24
    };
25
  }
26
};
27
 
28
// Use in tool
29
execute: async ({ code }) => {
30
  return safeExecute(async () => {
31
    const sandbox = await Sandbox.create({ ... });
32
    // ... execution code
33
  });
34
},
35
 

Production Considerations

1. Rate Limiting

typescript
1
import { Ratelimit } from '@upstash/ratelimit';
2
import { Redis } from '@upstash/redis';
3
 
4
const ratelimit = new Ratelimit({
5
  redis: Redis.fromEnv(),
6
  limiter: Ratelimit.slidingWindow(10, '1 m'), // 10 requests per minute
7
});
8
 
9
export async function POST(req: Request) {
10
  const ip = req.headers.get('x-forwarded-for') ?? 'anonymous';
11
  const { success } = await ratelimit.limit(ip);
12
  
13
  if (!success) {
14
    return new Response('Rate limit exceeded', { status: 429 });
15
  }
16
  
17
  // ... rest of handler
18
}
19
 

2. Input Validation

typescript
1
const MAX_CODE_LENGTH = 10000;
2
 
3
execute: async ({ code }) => {
4
  if (code.length > MAX_CODE_LENGTH) {
5
    return {
6
      success: false,
7
      error: `Code exceeds maximum length of ${MAX_CODE_LENGTH} characters`,
8
    };
9
  }
10
  // ... execution
11
},
12
 

3. Sandbox Pooling

typescript
1
// For high-traffic applications, maintain a pool of warm sandboxes
2
class SandboxPool {
3
  private pool: Sandbox[] = [];
4
  private maxSize = 5;
5
  
6
  async acquire(): Promise<Sandbox> {
7
    if (this.pool.length > 0) {
8
      return this.pool.pop()!;
9
    }
10
    return Sandbox.create({ template: 'code-interpreter' });
11
  }
12
  
13
  release(sandbox: Sandbox) {
14
    if (this.pool.length < this.maxSize) {
15
      this.pool.push(sandbox);
16
    } else {
17
      sandbox.kill();
18
    }
19
  }
20
}
21
 

Complete Example

Here's a full working Next.js app:

typescript
1
// app/api/chat/route.ts
2
import { openai } from '@ai-sdk/openai';
3
import { streamText, tool } from 'ai';
4
import { z } from 'zod';
5
import { Sandbox } from '@hopx-ai/sdk';
6
 
7
export const maxDuration = 60;
8
 
9
export async function POST(req: Request) {
10
  const { messages } = await req.json();
11
 
12
  const result = streamText({
13
    model: openai('gpt-4o'),
14
    system: `You are a helpful coding assistant. Execute Python code to answer questions.`,
15
    messages,
16
    tools: {
17
      python: tool({
18
        description: 'Execute Python code',
19
        parameters: z.object({ code: z.string() }),
20
        execute: async ({ code }) => {
21
          const sandbox = await Sandbox.create({
22
            template: 'code-interpreter',
23
            apiKey: process.env.HOPX_API_KEY,
24
          });
25
          try {
26
            const result = await sandbox.runCode(code, {
27
              language: 'python',
28
              timeout: 30,
29
            });
30
            return {
31
              output: result.stdout || 'No output',
32
              error: result.stderr,
33
              success: result.exitCode === 0,
34
            };
35
          } finally {
36
            await sandbox.kill();
37
          }
38
        },
39
      }),
40
    },
41
    maxSteps: 5,
42
  });
43
 
44
  return result.toDataStreamResponse();
45
}
46
 
tsx
1
// app/page.tsx
2
'use client';
3
 
4
import { useChat } from 'ai/react';
5
 
6
export default function Home() {
7
  const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat();
8
 
9
  return (
10
    <div className="max-w-2xl mx-auto p-4">
11
      <h1 className="text-2xl font-bold mb-4">AI Code Assistant</h1>
12
      
13
      <div className="space-y-4 mb-4">
14
        {messages.map((m) => (
15
          <div key={m.id} className={m.role === 'user' ? 'text-right' : ''}>
16
            <div className={`inline-block p-3 rounded-lg ${
17
              m.role === 'user' ? 'bg-blue-600 text-white' : 'bg-gray-100'
18
            }`}>
19
              <p>{m.content}</p>
20
              {m.toolInvocations?.map((t) => (
21
                <pre key={t.toolCallId} className="mt-2 p-2 bg-black text-green-400 rounded text-sm">
22
                  {'result' in t ? t.result.output : 'Executing...'}
23
                </pre>
24
              ))}
25
            </div>
26
          </div>
27
        ))}
28
      </div>
29
 
30
      <form onSubmit={handleSubmit} className="flex gap-2">
31
        <input
32
          value={input}
33
          onChange={handleInputChange}
34
          placeholder="Ask me to calculate something..."
35
          className="flex-1 p-2 border rounded"
36
        />
37
        <button 
38
          type="submit" 
39
          disabled={isLoading}
40
          className="px-4 py-2 bg-blue-600 text-white rounded disabled:opacity-50"
41
        >
42
          Send
43
        </button>
44
      </form>
45
    </div>
46
  );
47
}
48
 

Conclusion

The Vercel AI SDK + HopX combination gives you:

  • Streaming responses: Users see AI thinking in real-time
  • Secure execution: Code runs in isolated sandboxes
  • Tool calling: Clean integration with function calling
  • React-first: Hooks that work seamlessly with Next.js

This pattern works for any AI application that needs to execute untrusted code—from coding assistants to data analysis tools.


Ready to add code execution to your AI app? Get started with HopX — sandboxes that spin up in 100ms.

Further Reading