Initial commit
This commit is contained in:
171
templates/nextjs/api-agent-route.ts
Normal file
171
templates/nextjs/api-agent-route.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* Next.js App Router API Route with OpenAI Agents
|
||||
*
|
||||
* File: app/api/agent/route.ts
|
||||
*
|
||||
* Demonstrates:
|
||||
* - Creating API routes with agents
|
||||
* - Handling POST requests
|
||||
* - Streaming responses
|
||||
* - Error handling
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { Agent, run, tool } from '@openai/agents';
|
||||
|
||||
// ========================================
|
||||
// Agent Definition
|
||||
// ========================================
|
||||
|
||||
const searchTool = tool({
|
||||
name: 'search',
|
||||
description: 'Search for information',
|
||||
parameters: z.object({
|
||||
query: z.string(),
|
||||
}),
|
||||
execute: async ({ query }) => {
|
||||
// Implement your search logic
|
||||
return `Search results for: ${query}`;
|
||||
},
|
||||
});
|
||||
|
||||
const assistantAgent = new Agent({
|
||||
name: 'Assistant',
|
||||
instructions: 'You are a helpful assistant. Use the search tool when you need to find information.',
|
||||
tools: [searchTool],
|
||||
model: 'gpt-4o-mini',
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// POST /api/agent
|
||||
// ========================================
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Parse request body
|
||||
const body = await request.json();
|
||||
const { message, stream = false } = body;
|
||||
|
||||
if (!message || typeof message !== 'string') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid message' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Non-Streaming Response
|
||||
// ========================================
|
||||
|
||||
if (!stream) {
|
||||
const result = await run(assistantAgent, message, {
|
||||
maxTurns: 5,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
response: result.finalOutput,
|
||||
agent: result.currentAgent?.name,
|
||||
tokens: result.usage.totalTokens,
|
||||
history: result.history.length,
|
||||
});
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Streaming Response
|
||||
// ========================================
|
||||
|
||||
const streamResult = await run(assistantAgent, message, {
|
||||
stream: true,
|
||||
maxTurns: 5,
|
||||
});
|
||||
|
||||
// Create readable stream
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
try {
|
||||
for await (const event of streamResult) {
|
||||
if (event.type === 'raw_model_stream_event') {
|
||||
const chunk = event.data?.choices?.[0]?.delta?.content || '';
|
||||
if (chunk) {
|
||||
controller.enqueue(
|
||||
encoder.encode(`data: ${JSON.stringify({ type: 'chunk', content: chunk })}\n\n`)
|
||||
);
|
||||
}
|
||||
|
||||
} else if (event.type === 'agent_updated_stream_event') {
|
||||
controller.enqueue(
|
||||
encoder.encode(`data: ${JSON.stringify({
|
||||
type: 'agent_change',
|
||||
agent: event.agent.name
|
||||
})}\n\n`)
|
||||
);
|
||||
|
||||
} else if (event.type === 'run_item_stream_event') {
|
||||
if (event.name === 'tool_call') {
|
||||
controller.enqueue(
|
||||
encoder.encode(`data: ${JSON.stringify({
|
||||
type: 'tool_call',
|
||||
name: (event.item as any).name,
|
||||
arguments: (event.item as any).arguments,
|
||||
})}\n\n`)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await streamResult.completed;
|
||||
|
||||
// Send completion event
|
||||
controller.enqueue(
|
||||
encoder.encode(`data: ${JSON.stringify({
|
||||
type: 'done',
|
||||
tokens: streamResult.result.usage.totalTokens
|
||||
})}\n\n`)
|
||||
);
|
||||
|
||||
controller.close();
|
||||
|
||||
} catch (error: any) {
|
||||
controller.enqueue(
|
||||
encoder.encode(`data: ${JSON.stringify({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
})}\n\n`)
|
||||
);
|
||||
controller.close();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
},
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('API route error:', error);
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// GET /api/agent (Info)
|
||||
// ========================================
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json({
|
||||
agent: assistantAgent.name,
|
||||
tools: assistantAgent.tools?.map((t: any) => t.name) || [],
|
||||
model: assistantAgent.model,
|
||||
});
|
||||
}
|
||||
162
templates/nextjs/api-realtime-route.ts
Normal file
162
templates/nextjs/api-realtime-route.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* Next.js API Route for Realtime Voice Agent
|
||||
*
|
||||
* File: app/api/voice/session/route.ts
|
||||
*
|
||||
* Demonstrates:
|
||||
* - Generating ephemeral API keys for voice sessions
|
||||
* - Securing realtime agent access
|
||||
* - NEVER exposing main API key to clients
|
||||
*
|
||||
* CRITICAL: Never send your main OPENAI_API_KEY to the browser!
|
||||
* Use ephemeral session keys with short expiration.
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import OpenAI from 'openai';
|
||||
|
||||
// ========================================
|
||||
// POST /api/voice/session
|
||||
// Generate ephemeral API key for voice session
|
||||
// ========================================
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Optional: Authenticate user first
|
||||
// const session = await getServerSession(authOptions);
|
||||
// if (!session) {
|
||||
// return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
// }
|
||||
|
||||
// Create OpenAI client with main API key (server-side only)
|
||||
const openai = new OpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
});
|
||||
|
||||
// Generate ephemeral key
|
||||
// NOTE: As of 2025-10-26, OpenAI doesn't have a dedicated ephemeral key endpoint
|
||||
// You may need to use session tokens or implement your own proxy
|
||||
//
|
||||
// Recommended approach: Create a proxy that validates requests and
|
||||
// forwards to OpenAI API with your key server-side
|
||||
|
||||
// For demonstration, we'll show the pattern:
|
||||
// In production, implement a secure proxy or use OpenAI's ephemeral keys when available
|
||||
|
||||
// Option 1: Return a session token (your own implementation)
|
||||
const sessionToken = generateSecureSessionToken();
|
||||
|
||||
// Store session token mapping to your API key in Redis/KV
|
||||
// await redis.set(`session:${sessionToken}`, process.env.OPENAI_API_KEY, {
|
||||
// ex: 3600, // 1 hour expiration
|
||||
// });
|
||||
|
||||
return NextResponse.json({
|
||||
sessionToken,
|
||||
expiresIn: 3600, // seconds
|
||||
});
|
||||
|
||||
// Option 2: If OpenAI provides ephemeral keys API (future)
|
||||
// const ephemeralKey = await openai.ephemeralKeys.create({
|
||||
// expiresIn: 3600,
|
||||
// });
|
||||
// return NextResponse.json({
|
||||
// apiKey: ephemeralKey.key,
|
||||
// expiresIn: ephemeralKey.expiresIn,
|
||||
// });
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Session creation error:', error);
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to create session' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Helper: Generate Secure Session Token
|
||||
// ========================================
|
||||
|
||||
function generateSecureSessionToken(): string {
|
||||
// Generate cryptographically secure random token
|
||||
const array = new Uint8Array(32);
|
||||
crypto.getRandomValues(array);
|
||||
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Proxy Endpoint (Recommended Pattern)
|
||||
// File: app/api/voice/proxy/route.ts
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* This proxy validates session tokens and forwards requests to OpenAI API
|
||||
* This is the RECOMMENDED approach to avoid exposing your API key
|
||||
*/
|
||||
|
||||
export async function POST_PROXY(request: NextRequest) {
|
||||
try {
|
||||
// Get session token from request
|
||||
const authHeader = request.headers.get('authorization');
|
||||
const sessionToken = authHeader?.replace('Bearer ', '');
|
||||
|
||||
if (!sessionToken) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Validate session token
|
||||
// const apiKey = await redis.get(`session:${sessionToken}`);
|
||||
// if (!apiKey) {
|
||||
// return NextResponse.json({ error: 'Invalid session' }, { status: 401 });
|
||||
// }
|
||||
|
||||
// Get the actual OpenAI API request from client
|
||||
const body = await request.json();
|
||||
|
||||
// Forward to OpenAI API with server-side key
|
||||
const openai = new OpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
});
|
||||
|
||||
// Forward request to OpenAI Realtime API
|
||||
// Implementation depends on the exact endpoint being called
|
||||
// This is a simplified example
|
||||
|
||||
return NextResponse.json({
|
||||
message: 'Proxy implementation needed',
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Proxy error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Important Security Notes
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* SECURITY CHECKLIST:
|
||||
*
|
||||
* 1. ✅ NEVER send OPENAI_API_KEY to browser
|
||||
* 2. ✅ Use ephemeral/session tokens with expiration
|
||||
* 3. ✅ Implement rate limiting per user/session
|
||||
* 4. ✅ Authenticate users before generating tokens
|
||||
* 5. ✅ Store session tokens in secure storage (Redis/KV)
|
||||
* 6. ✅ Log all voice session creation for monitoring
|
||||
* 7. ✅ Set maximum session duration (e.g., 1 hour)
|
||||
* 8. ✅ Implement cost controls and usage tracking
|
||||
*
|
||||
* RECOMMENDED ARCHITECTURE:
|
||||
*
|
||||
* Browser Client → Next.js Proxy → OpenAI API
|
||||
* ↓
|
||||
* Session Token (never sees main API key)
|
||||
*
|
||||
* Alternative: Use OpenAI's official ephemeral key endpoint when available
|
||||
*/
|
||||
Reference in New Issue
Block a user