AI Provider Integration
When to Use
Use Drupal AI module when available. Use direct provider integration for simple Next.js apps. Abstract with provider interface for multiple providers.
Decision
| Scenario | Pattern |
|---|---|
| Drupal AI module | Let Drupal handle provider integration |
| Simple Next.js app | Direct provider integration |
| Multiple providers | Abstract with provider interface |
| Custom models | Implement compatible API wrapper |
OpenAI Integration
Direct OpenAI (No Drupal):
// app/api/chat/route.ts
export async function POST(request: NextRequest) {
const body = await request.json();
const lastMessage = body.messages[body.messages.length - 1];
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'gpt-4',
messages: body.messages.map(m => ({
role: m.role === 'ai' ? 'assistant' : 'user',
content: m.text,
})),
stream: body.stream === 1,
}),
});
if (body.stream === 1) {
// Stream response (see streaming guide)
return streamOpenAIResponse(response);
}
const data = await response.json();
return NextResponse.json({
html: data.choices[0].message.content,
});
}
OpenAI Streaming:
async function streamOpenAIResponse(response: Response) {
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const stream = new ReadableStream({
async start(controller) {
const reader = response.body!.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n').filter(line => line.trim());
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') continue;
try {
const parsed = JSON.parse(data);
const content = parsed.choices[0]?.delta?.content;
if (content) {
const sseMessage = `data: ${JSON.stringify({ html: content })}\n\n`;
controller.enqueue(encoder.encode(sseMessage));
}
} catch (e) {
// Skip invalid JSON
}
}
}
}
} finally {
controller.close();
}
},
});
return new NextResponse(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
}
Anthropic Integration
Direct Anthropic:
export async function POST(request: NextRequest) {
const body = await request.json();
const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'x-api-key': process.env.ANTHROPIC_API_KEY!,
'anthropic-version': '2023-06-01',
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'claude-3-5-sonnet-20241022',
max_tokens: 1024,
messages: body.messages.map(m => ({
role: m.role === 'ai' ? 'assistant' : 'user',
content: m.text,
})),
stream: body.stream === 1,
}),
});
if (body.stream === 1) {
return streamAnthropicResponse(response);
}
const data = await response.json();
return NextResponse.json({
html: data.content[0].text,
});
}
Anthropic Streaming:
async function streamAnthropicResponse(response: Response) {
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const stream = new ReadableStream({
async start(controller) {
const reader = response.body!.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n').filter(line => line.trim());
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
try {
const parsed = JSON.parse(data);
if (parsed.type === 'content_block_delta') {
const content = parsed.delta?.text;
if (content) {
const sseMessage = `data: ${JSON.stringify({ html: content })}\n\n`;
controller.enqueue(encoder.encode(sseMessage));
}
}
} catch (e) {
// Skip invalid JSON
}
}
}
}
} finally {
controller.close();
}
},
});
return new NextResponse(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
}
Pattern
// Provider abstraction
interface AIProvider {
sendMessage(messages: any[], stream: boolean): Promise<Response>;
}
class OpenAIProvider implements AIProvider {
async sendMessage(messages: any[], stream: boolean) {
return fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'gpt-4',
messages,
stream,
}),
});
}
}
class AnthropicProvider implements AIProvider {
async sendMessage(messages: any[], stream: boolean) {
return fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'x-api-key': process.env.ANTHROPIC_API_KEY!,
'anthropic-version': '2023-06-01',
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'claude-3-5-sonnet-20241022',
max_tokens: 1024,
messages,
stream,
}),
});
}
}
Common Mistakes
- Wrong: Exposing API keys client-side → Right: NEVER put API keys in frontend code. Always use server routes
- Wrong: Not handling rate limits → Right: Implement exponential backoff and error handling
- Wrong: Forgetting to transform message roles → Right: OpenAI uses 'assistant', DeepChat uses 'ai'. Map correctly
- Wrong: Not validating API key presence → Right: Check
process.env.OPENAI_API_KEYexists before calling - Wrong: Mixing streaming formats → Right: OpenAI and Anthropic have different SSE formats. Parse correctly
- Wrong: Not handling stream errors → Right: Streams can fail mid-response. Always close gracefully
See Also
- Streaming Responses
- Security
- Reference: https://platform.openai.com/docs/guides/streaming-responses
- Reference: https://docs.anthropic.com/en/api/messages-streaming