1- /**
2- * Sandbox Creation API Endpoint
3- *
4- * Creates a Vercel Sandbox on-demand to host the LiveKit voice agent.
5- * This endpoint:
6- * 1. Creates a new sandbox with specified runtime
7- * 2. Clones the agent repository
8- * 3. Installs dependencies
9- * 4. Starts the agent process with LiveKit credentials
10- * 5. Returns sandbox information
11- */
121import { NextResponse } from 'next/server' ;
132import { Sandbox } from '@vercel/sandbox' ;
143import { sandboxManager } from '@/lib/sandbox-manager' ;
15- import type { CreateSandboxRequest , CreateSandboxResponse , SandboxInfo } from '@/types/sandbox' ;
16-
17- // Configuration type
18- type SandboxConfig = {
19- repoUrl : string ;
20- runtime : string ;
21- timeout : number ;
22- vcpus : number ;
23- githubToken ?: string ;
24- } ;
4+ import type {
5+ CreateSandboxRequest ,
6+ CreateSandboxResponse ,
7+ SandboxInfo
8+ } from '@/types/sandbox' ;
259
26- // Default configuration
2710const DEFAULT_CONFIG = {
28- repoUrl :
29- process . env . AGENT_REPO_URL || 'https://github.com/livekit-examples/agent-starter-python.git' ,
11+ repoUrl : process . env . AGENT_REPO_URL ||
12+ 'https://github.com/livekit-examples/agent-starter-python.git' ,
3013 runtime : ( process . env . AGENT_RUNTIME as string ) || 'python3.13' ,
31- timeout : parseInt ( process . env . SANDBOX_TIMEOUT || '600000' , 10 ) , // 10 minutes
14+ timeout : parseInt ( process . env . SANDBOX_TIMEOUT || '600000' , 10 ) ,
3215 vcpus : 4 ,
3316} ;
3417
@@ -37,7 +20,6 @@ export async function POST(req: Request) {
3720 const body : CreateSandboxRequest = await req . json ( ) ;
3821 const config = { ...DEFAULT_CONFIG , ...body . config } ;
3922
40- // Validate required environment variables
4123 if (
4224 ! process . env . LIVEKIT_API_KEY ||
4325 ! process . env . LIVEKIT_API_SECRET ||
@@ -47,76 +29,62 @@ export async function POST(req: Request) {
4729 {
4830 success : false ,
4931 error : 'LiveKit credentials not configured' ,
50- details :
51- 'Please set LIVEKIT_API_KEY, LIVEKIT_API_SECRET, and LIVEKIT_URL environment variables' ,
32+ details : 'Please set LIVEKIT_API_KEY, LIVEKIT_API_SECRET, ' +
33+ 'and LIVEKIT_URL environment variables' ,
5234 } as CreateSandboxResponse ,
5335 { status : 500 }
5436 ) ;
5537 }
5638
57- console . log ( ` [Sandbox] Creating sandbox with config:` , {
39+ console . log ( ' [Sandbox] Creating with config:' , {
5840 runtime : config . runtime ,
5941 repoUrl : config . repoUrl ,
6042 timeout : config . timeout ,
6143 } ) ;
6244
63- // Generate room name if not provided
64- const roomName =
65- body . roomName || `voice_agent_sandbox_${ Math . random ( ) . toString ( 36 ) . substring ( 7 ) } ` ;
45+ const roomName = body . roomName ||
46+ `voice_agent_sandbox_${ Math . random ( ) . toString ( 36 ) . substring ( 7 ) } ` ;
6647
67- // Create sandbox info
6848 const sandboxInfo : SandboxInfo = {
69- id : '' , // Will be set after creation
49+ id : '' ,
7050 status : 'creating' ,
7151 roomName,
7252 agentName : 'sandbox-agent' ,
7353 createdAt : Date . now ( ) ,
7454 expiresAt : Date . now ( ) + config . timeout ,
7555 } ;
7656
77- // Register sandbox
7857 sandboxManager . register ( sandboxInfo ) ;
7958
8059 try {
81- // Create the Vercel Sandbox
8260 const sandbox = await Sandbox . create ( {
8361 source : {
8462 url : config . repoUrl ,
8563 type : 'git' ,
86- // For private repositories, add authentication
87- ...( config . githubToken
88- ? {
89- username : 'x-access-token' ,
90- password : config . githubToken ,
91- }
92- : { } ) ,
64+ ...( config . githubToken ? {
65+ username : 'x-access-token' ,
66+ password : config . githubToken ,
67+ } : { } ) ,
9368 } ,
9469 runtime : config . runtime ,
9570 resources : { vcpus : config . vcpus } ,
9671 timeout : config . timeout ,
97- ports : [ ] , // Agent doesn't expose HTTP ports
9872 } ) ;
9973
100- // Get sandbox ID - the SDK uses .sandboxId property
101- const actualSandboxId = ( sandbox as { sandboxId : string } ) . sandboxId ;
102- sandboxInfo . id = actualSandboxId ;
74+ const sandboxId = ( sandbox as { sandboxId : string } ) . sandboxId ;
75+ sandboxInfo . id = sandboxId ;
10376 sandboxManager . register ( sandboxInfo ) ;
10477
105- console . log ( `[Sandbox ${ actualSandboxId } ] Created successfully` ) ;
78+ console . log ( `[Sandbox ${ sandboxId } ] Created successfully` ) ;
10679
107- // Return immediately so frontend can start polling for progress
108- // Continue setup in the background
109- setupSandbox ( sandbox , actualSandboxId , config , sandboxInfo ) . catch ( ( error ) => {
110- console . error ( `[Sandbox ${ actualSandboxId } ] Setup failed:` , error ) ;
111- sandboxManager . updateStatus ( actualSandboxId , 'failed' , error . message ) ;
80+ setupSandbox ( sandbox , sandboxId , config , sandboxInfo ) . catch ( ( error ) => {
81+ console . error ( `[Sandbox ${ sandboxId } ] Setup failed:` , error ) ;
82+ sandboxManager . updateStatus ( sandboxId , 'failed' , error . message ) ;
11283 } ) ;
11384
11485 return NextResponse . json ( {
11586 success : true ,
116- sandbox : {
117- ...sandboxInfo ,
118- id : actualSandboxId ,
119- } ,
87+ sandbox : { ...sandboxInfo , id : sandboxId } ,
12088 } as CreateSandboxResponse ) ;
12189 } catch ( error ) {
12290 const errorMessage = error instanceof Error ? error . message : 'Unknown error' ;
@@ -150,206 +118,109 @@ export async function POST(req: Request) {
150118 }
151119}
152120
153- // Background setup function
121+ type RunCommandFn = ( args : {
122+ cmd : string ;
123+ args : string [ ] ;
124+ stdout ?: NodeJS . WriteStream ;
125+ stderr ?: NodeJS . WriteStream ;
126+ detached ?: boolean ;
127+ env ?: Record < string , string > ;
128+ } ) => Promise < { exitCode : number } > ;
129+
154130async function setupSandbox (
155- sandbox : {
156- runCommand : ( args : {
157- cmd : string ;
158- args : string [ ] ;
159- stdout ?: NodeJS . WriteStream ;
160- stderr ?: NodeJS . WriteStream ;
161- detached ?: boolean ;
162- env ?: Record < string , string > ;
163- } ) => Promise < { exitCode : number } > ;
164- } ,
131+ sandbox : { runCommand : RunCommandFn } ,
165132 sandboxId : string ,
166- config : SandboxConfig ,
133+ config : typeof DEFAULT_CONFIG & { githubToken ?: string } ,
167134 sandboxInfo : SandboxInfo
168135) {
169- try {
170- // Update status to installing
136+ const run = ( cmd : string , args : string [ ] , opts : Record < string , unknown > = { } ) =>
137+ sandbox . runCommand ( { cmd, args, ...opts } ) ;
138+
139+ const updateStatus = ( status : string , msg : string ) =>
171140 sandboxManager . updateStatus (
172141 sandboxId ,
173- 'installing' ,
142+ status as 'installing' | 'starting' | 'ready' | 'failed ',
174143 undefined ,
175- 'Cloning repository and setting up environment'
144+ msg
176145 ) ;
177146
178- // First, check if files were cloned correctly
179- console . log ( `[Sandbox ${ sandboxId } ] Checking repository contents...` ) ;
180-
181- await sandbox . runCommand ( {
182- cmd : 'ls' ,
183- args : [ '-la' ] ,
184- stdout : process . stdout ,
185- stderr : process . stderr ,
186- } ) ;
147+ try {
148+ updateStatus ( 'installing' , 'Setting up environment' ) ;
187149
188- // Check what Python executables are available
189- console . log ( `[Sandbox ${ sandboxId } ] Checking available Python executables...` ) ;
190- await sandbox . runCommand ( {
191- cmd : 'sh' ,
192- args : [ '-c' , 'which python3 && python3 --version && which pip && pip --version' ] ,
193- stdout : process . stdout ,
194- stderr : process . stderr ,
195- } ) ;
150+ await run ( 'ls' , [ '-la' ] ) ;
196151
197- // Install dependencies based on runtime
198- let installCmd ;
152+ let installCmd : { cmd : string ; args : string [ ] } ;
199153
200154 if ( config . runtime === 'python3.13' ) {
201- // Check if pyproject.toml exists (modern Python project)
202- const hasPyproject = await sandbox . runCommand ( {
203- cmd : 'test' ,
204- args : [ '-f' , 'pyproject.toml' ] ,
205- } ) ;
155+ const hasPyproject = await run ( 'test' , [ '-f' , 'pyproject.toml' ] ) ;
206156
207157 if ( hasPyproject . exitCode === 0 ) {
208- // Modern Python project with pyproject.toml
209- console . log ( `[Sandbox ${ sandboxId } ] Detected pyproject.toml, installing with pip...` ) ;
158+ console . log ( `[Sandbox ${ sandboxId } ] Using pyproject.toml` ) ;
210159 installCmd = { cmd : 'pip' , args : [ 'install' , '--user' , '.' ] } ;
211160 } else {
212- // Traditional requirements.txt
213- installCmd = { cmd : 'pip' , args : [ 'install' , '--user' , '-r' , 'requirements.txt' ] } ;
161+ installCmd = {
162+ cmd : 'pip' ,
163+ args : [ 'install' , '--user' , '-r' , 'requirements.txt' ]
164+ } ;
214165 }
215166 } else {
216- // Node.js
217167 installCmd = { cmd : 'npm' , args : [ 'install' ] } ;
218168 }
219169
220- console . log ( `[Sandbox ${ sandboxId } ] Installing dependencies...` ) ;
221- sandboxManager . updateStatus (
222- sandboxId ,
223- 'installing' ,
224- undefined ,
225- 'Installing Python dependencies and packages'
226- ) ;
227-
228- const install = await sandbox . runCommand ( {
229- ...installCmd ,
230- stdout : process . stdout ,
231- stderr : process . stderr ,
232- } ) ;
170+ updateStatus ( 'installing' , 'Installing dependencies' ) ;
233171
172+ const install = await run ( installCmd . cmd , installCmd . args ) ;
234173 if ( install . exitCode !== 0 ) {
235- const error = 'Dependency installation failed' ;
236- sandboxManager . updateStatus ( sandboxId , 'failed' , error ) ;
237- throw new Error ( `${ error } : Exit code ${ install . exitCode } ` ) ;
174+ throw new Error ( `Dependency installation failed: Exit code ${ install . exitCode } ` ) ;
238175 }
239176
240177 console . log ( `[Sandbox ${ sandboxId } ] Dependencies installed` ) ;
241178
242- // Install SSL certificates for Python (fixes certificate verification errors)
243179 if ( config . runtime === 'python3.13' ) {
244- console . log ( `[Sandbox ${ sandboxId } ] Installing SSL certificates...` ) ;
245- sandboxManager . updateStatus (
246- sandboxId ,
247- 'installing' ,
248- undefined ,
249- 'Configuring SSL certificates'
250- ) ;
251-
252- const certInstall = await sandbox . runCommand ( {
253- cmd : '/vercel/runtimes/python/bin/pip3' ,
254- args : [ 'install' , '--upgrade' , 'certifi' ] ,
255- stdout : process . stdout ,
256- stderr : process . stderr ,
257- } ) ;
258-
259- if ( certInstall . exitCode !== 0 ) {
260- console . log ( `[Sandbox ${ sandboxId } ] SSL certificate installation failed, continuing...` ) ;
261- } else {
262- console . log ( `[Sandbox ${ sandboxId } ] SSL certificates installed` ) ;
263- }
180+ updateStatus ( 'installing' , 'Configuring SSL certificates' ) ;
181+ await run ( '/vercel/runtimes/python/bin/pip3' , [ 'install' , '--upgrade' , 'certifi' ] ) ;
182+
183+ updateStatus ( 'installing' , 'Downloading AI models' ) ;
184+ await run ( '/vercel/runtimes/python/bin/python3' , [
185+ 'src/agent.py' ,
186+ 'download-files'
187+ ] ) ;
264188 }
265189
266- // Download model files if needed (for Python agents)
267- if ( config . runtime === 'python3.13' ) {
268- console . log ( `[Sandbox ${ sandboxId } ] Downloading model files...` ) ;
269- sandboxManager . updateStatus (
270- sandboxId ,
271- 'installing' ,
272- undefined ,
273- 'Downloading AI models (this may take a moment)'
274- ) ;
275-
276- const downloadCmd = await sandbox . runCommand ( {
277- cmd : '/vercel/runtimes/python/bin/python3' ,
278- args : [ 'src/agent.py' , 'download-files' ] ,
279- stdout : process . stdout ,
280- stderr : process . stderr ,
281- } ) ;
282-
283- if ( downloadCmd . exitCode !== 0 ) {
284- console . log ( `[Sandbox ${ sandboxId } ] Model download failed (non-critical), continuing...` ) ;
285- } else {
286- console . log ( `[Sandbox ${ sandboxId } ] Model files downloaded` ) ;
287- }
288- }
190+ updateStatus ( 'starting' , 'Starting voice agent' ) ;
289191
290- // Update status to starting
291- sandboxManager . updateStatus (
292- sandboxId ,
293- 'starting' ,
294- undefined ,
295- 'Starting voice agent and connecting to LiveKit'
296- ) ;
297-
298- // Start agent process
299- let agentCmd ;
192+ let agentCmd : { cmd : string ; args : string [ ] } ;
300193
301194 if ( config . runtime === 'python3.13' ) {
302- // Check if agent is in src directory or root
303- const hasSrcAgent = await sandbox . runCommand ( {
304- cmd : 'test' ,
305- args : [ '-f' , 'src/agent.py' ] ,
306- } ) ;
307-
308- // Use the runtime-specific python from /vercel/runtimes/python
195+ const hasSrcAgent = await run ( 'test' , [ '-f' , 'src/agent.py' ] ) ;
309196 const pythonPath = '/vercel/runtimes/python/bin/python3' ;
310197
311- if ( hasSrcAgent . exitCode === 0 ) {
312- agentCmd = { cmd : pythonPath , args : [ 'src/agent.py' , 'start' ] } ;
313- } else {
314- agentCmd = { cmd : pythonPath , args : [ 'agent.py' , 'start' ] } ;
315- }
198+ agentCmd = hasSrcAgent . exitCode === 0
199+ ? { cmd : pythonPath , args : [ 'src/agent.py' , 'start' ] }
200+ : { cmd : pythonPath , args : [ 'agent.py' , 'start' ] } ;
316201 } else {
317- // Node.js
318202 agentCmd = { cmd : 'node' , args : [ 'agent.js' ] } ;
319203 }
320204
321- console . log (
322- `[Sandbox ${ sandboxId } ] Starting agent with command: ${ agentCmd . cmd } ${ agentCmd . args . join ( ' ' ) } ...`
323- ) ;
205+ console . log ( `[Sandbox ${ sandboxId } ] Starting agent: ${ agentCmd . cmd } ${ agentCmd . args . join ( ' ' ) } ` ) ;
324206
325207 await sandbox . runCommand ( {
326208 ...agentCmd ,
327- detached : true , // Run in background
209+ detached : true ,
328210 env : {
329211 LIVEKIT_API_KEY : process . env . LIVEKIT_API_KEY ! ,
330212 LIVEKIT_API_SECRET : process . env . LIVEKIT_API_SECRET ! ,
331213 LIVEKIT_URL : process . env . LIVEKIT_URL ! ,
332214 LIVEKIT_AGENT_NAME : sandboxInfo . agentName || 'sandbox-agent' ,
333- // AI service API keys (required by agent-starter-python)
334215 OPENAI_API_KEY : process . env . OPENAI_API_KEY || '' ,
335216 ASSEMBLYAI_API_KEY : process . env . ASSEMBLYAI_API_KEY || '' ,
336217 CARTESIA_API_KEY : process . env . CARTESIA_API_KEY || '' ,
337- // Add Python user site packages to PATH
338218 PYTHONPATH : '/home/vercel-sandbox/.local/lib/python3.13/site-packages' ,
339219 PATH : `${ process . env . PATH } :/home/vercel-sandbox/.local/bin` ,
340220 } ,
341- stdout : process . stdout ,
342- stderr : process . stderr ,
343221 } ) ;
344222
345- // Update status to ready
346- sandboxManager . updateStatus (
347- sandboxId ,
348- 'ready' ,
349- undefined ,
350- 'Agent is ready and waiting for connection'
351- ) ;
352-
223+ updateStatus ( 'ready' , 'Agent is ready' ) ;
353224 console . log ( `[Sandbox ${ sandboxId } ] Agent started successfully` ) ;
354225 } catch ( error ) {
355226 const errorMessage = error instanceof Error ? error . message : 'Unknown error' ;
0 commit comments