ignitionstack.pro connects to cloud LLM providers through secure, resilient adapters. This guide covers authentication, API key management, cost optimization, and production best practices.
| Provider | SDK | API Base | Status |
|---|---|---|---|
| OpenAI | openai | api.openai.com | Full support |
| Google Gemini | @google/generative-ai | generativelanguage.googleapis.com | Full support |
| Anthropic | @anthropic-ai/sdk | api.anthropic.com | Ready |
For platform-wide usage (server-side operations):
# .env.local / .env.production
OPENAI_API_KEY=sk-...
GOOGLE_AI_API_KEY=...
ANTHROPIC_API_KEY=sk-ant-...Users can bring their own API keys for direct billing:
// Server action: Save user's API key
// src/app/actions/ai/save-api-key.ts
import { encrypt } from '@/lib/ai/utils/encryption'
export async function saveAPIKey(provider: string, apiKey: string) {
const { user } = await requireAuth()
// Encrypt before storage
const encryptedKey = encrypt(apiKey, process.env.ENCRYPTION_KEY!)
await db.from('ai_user_api_keys').upsert({
user_id: user.id,
provider,
encrypted_key: encryptedKey,
key_hash: hash(apiKey), // For validation without decryption
is_active: true,
})
}All user API keys are encrypted with AES-256-GCM:
// src/app/lib/ai/utils/encryption.ts
// Structure: salt (64 bytes) || iv (16 bytes) || authTag (16 bytes) || ciphertext
// - Random salt per encryption for key derivation
// - Random IV per encryption for uniqueness
// - Auth tag for integrity verification
export function encrypt(plaintext: string, masterKey: string): string {
const salt = crypto.randomBytes(64)
const iv = crypto.randomBytes(16)
const key = crypto.pbkdf2Sync(masterKey, salt, 100000, 32, 'sha256')
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv)
const encrypted = Buffer.concat([
cipher.update(plaintext, 'utf8'),
cipher.final(),
])
const authTag = cipher.getAuthTag()
return Buffer.concat([salt, iv, authTag, encrypted]).toString('base64')
}// Full OpenAI setup
const openai = await ProviderFactory.create('openai', {
apiKey: process.env.OPENAI_API_KEY,
organization: process.env.OPENAI_ORG_ID, // Optional
baseURL: process.env.OPENAI_BASE_URL, // For proxies/Azure
timeout: 30000,
maxRetries: 3,
})
// With user's own key
const openai = await ProviderFactory.createWithUserKey(userId, 'openai')# .env.local
OPENAI_BASE_URL=https://your-resource.openai.azure.com
OPENAI_API_KEY=your-azure-key
OPENAI_API_VERSION=2024-02-01const gemini = await ProviderFactory.create('gemini', {
apiKey: process.env.GOOGLE_AI_API_KEY,
})# For Google Cloud Vertex AI instead of AI Studio
GOOGLE_CLOUD_PROJECT=your-project-id
GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.jsonconst anthropic = await ProviderFactory.create('anthropic', {
apiKey: process.env.ANTHROPIC_API_KEY,
baseURL: 'https://api.anthropic.com', // Or proxy URL
})// Cost estimation is built into each adapter
const response = await provider.chat(messages, options)
console.log({
promptTokens: response.usage.promptTokens,
completionTokens: response.usage.completionTokens,
estimatedCost: response.usage.estimatedCost, // In USD
})| Provider | Model | Input (1M) | Output (1M) |
|---|---|---|---|
| OpenAI | gpt-4o | $2.50 | $10.00 |
| OpenAI | gpt-4o-mini | $0.15 | $0.60 |
| Gemini | gemini-2.0-flash | $0.075 | $0.30 |
| Gemini | gemini-1.5-pro | $1.25 | $5.00 |
| Anthropic | claude-3.5-sonnet | $3.00 | $15.00 |
-- Query usage from logs
SELECT
provider,
model,
DATE(created_at) as date,
SUM(prompt_tokens) as prompt_tokens,
SUM(completion_tokens) as completion_tokens,
SUM(estimated_cost) as total_cost
FROM ai_usage_logs
WHERE user_id = ?
GROUP BY provider, model, DATE(created_at)
ORDER BY date DESC;// Implement spending limits per user
const usage = await getMonthlyUsage(userId)
if (usage.totalCost >= user.plan.monthlyLimit) {
throw new Error('Monthly AI budget exceeded')
}
// Or per-request limits
if (estimatedCost > user.plan.maxRequestCost) {
throw new Error('Request would exceed cost limit')
}| Provider | Requests/min | Tokens/min | Tokens/day |
|---|---|---|---|
| OpenAI (Tier 1) | 500 | 30,000 | 200,000 |
| OpenAI (Tier 4) | 10,000 | 800,000 | 10M |
| Gemini (Free) | 15 | 1M | 1.5M |
| Gemini (Paid) | 1,000 | 4M | Unlimited |
// src/app/lib/api-rate-limit.ts
import { rateLimit } from '@/lib/rate-limit'
// Per-user limits
const aiRateLimit = rateLimit({
interval: 60 * 1000, // 1 minute
uniqueTokenPerInterval: 500,
})
export async function checkAIRateLimit(userId: string) {
const limit = await aiRateLimit.check(userId, 20) // 20 req/min
if (!limit.success) {
throw new RateLimitError({
retryAfter: limit.reset,
remaining: limit.remaining,
})
}
}// src/app/api/ai/chat/route.ts
export async function POST(request: Request) {
// ... authentication & validation ...
const stream = new TransformStream()
const writer = stream.writable.getWriter()
const encoder = new TextEncoder()
// Start streaming in background
(async () => {
try {
for await (const chunk of provider.chatStream(messages, options)) {
await writer.write(
encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`)
)
}
await writer.write(encoder.encode('data: [DONE]\n\n'))
} finally {
await writer.close()
}
})()
return new Response(stream.readable, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
})
}// React hook for streaming
const response = await fetch('/api/ai/chat', {
method: 'POST',
body: JSON.stringify({ message }),
})
const reader = response.body.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
const chunk = decoder.decode(value)
const lines = chunk.split('\n')
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = JSON.parse(line.slice(6))
if (data !== '[DONE]') {
onToken(data.content)
}
}
}
}try {
const response = await provider.chat(messages, options)
} catch (error) {
switch (error.code) {
case 'rate_limit_exceeded':
// Wait and retry
await sleep(error.retryAfter * 1000)
return retry()
case 'invalid_api_key':
// Notify user to update key
throw new AuthError('Invalid API key')
case 'context_length_exceeded':
// Truncate messages and retry
return retry(truncateMessages(messages))
case 'content_filter':
// Content was blocked by safety filters
throw new ModerationError('Content blocked')
default:
// Log and fallback to another provider
logger.error('Provider error', { error })
return fallbackProvider.chat(messages, options)
}
}// Built into adapters with exponential backoff
const response = await retry(
() => provider.chat(messages, options),
{
retries: 3,
factor: 2,
minTimeout: 1000,
maxTimeout: 10000,
onRetry: (error, attempt) => {
logger.warn('Retrying AI request', { attempt, error })
},
}
)// BAD - Never do this
const response = await fetch('https://api.openai.com/...', {
headers: { Authorization: `Bearer ${apiKey}` }, // Exposed!
})
// GOOD - Always proxy through your API
const response = await fetch('/api/ai/chat', {
body: JSON.stringify({ message }),
})// Server action with Zod validation
const schema = z.object({
message: z.string().min(1).max(10000),
conversationId: z.string().uuid(),
model: z.enum(['gpt-4o', 'gpt-4o-mini', 'gemini-1.5-pro']),
})
export async function sendMessage(input: unknown) {
const data = schema.parse(input)
// ... proceed with validated data
}// Check content before sending to LLM
const moderation = await openai.moderations.create({
input: userMessage,
})
if (moderation.results[0].flagged) {
throw new ModerationError('Content violates guidelines')
}// Log all AI operations
await db.from('ai_usage_logs').insert({
user_id: userId,
provider,
model,
operation: 'chat',
prompt_tokens: usage.promptTokens,
completion_tokens: usage.completionTokens,
estimated_cost: cost,
request_id: requestId,
ip_address: getClientIP(request),
})The Circuit Breaker automatically handles provider failures:
// Automatic failover when OpenAI is down
const router = new StrategyRouter({
fallbackOrder: ['openai', 'gemini', 'ollama'],
})
// If OpenAI circuit is open, routes to Gemini
const decision = await router.route(context)