Last verified: July 20, 2026
A Node.js service can call supported GPT, Claude and Gemini models through one LumeAPI OpenAI client. Keep the same baseURL, API key and USD wallet, then select an allowlisted model ID for each task. The production difference from a basic Node quickstart is policy: centralize model configuration, isolate per-model failures, compare models with the same task and log which route actually ran.
Reddit discussions about multi-model gateways repeatedly describe the same high-intent job: one authenticated endpoint for several model families without maintaining separate accounts and clients. This guide answers that implementation task; it does not create a separate page for every phrase such as “one API,” “unified gateway” or “multi-provider SDK.”
Install and configure one server-side client
npm install openai
export LUMEAPI_KEY='your-lumeapi-key'Create lume-models.mjs:
import OpenAI from 'openai';
export const client = new OpenAI({
apiKey: process.env.LUMEAPI_KEY,
baseURL: 'https://api.lumeapi.site/v1',
timeout: 30_000,
maxRetries: 1,
});
export const MODELS = Object.freeze({
gpt: 'gpt-5.4-mini',
claude: 'claude-sonnet-4-6',
gemini: 'gemini-3.5-flash',
});JavaScript uses baseURL, not Python's base_url. Keep the key on the server; browser-delivered code cannot keep a secret.
Route tasks through an explicit allowlist
Do not accept a raw model string from an end user. Map a stable internal task to an evaluated external model.
const TASK_ROUTES = Object.freeze({
classifyTicket: MODELS.gpt,
reviewDraft: MODELS.claude,
interactiveReply: MODELS.gemini,
});
export function modelForTask(task) {
const model = TASK_ROUTES[task];
if (!model) {
throw new Error(`Unknown AI task: ${task}`);
}
return model;
}
export async function generateForTask(task, prompt) {
const model = modelForTask(task);
const response = await client.chat.completions.create({
model,
messages: [{ role: 'user', content: prompt }],
});
return {
model,
text: response.choices[0]?.message?.content ?? '',
usage: response.usage ?? null,
requestId: response._request_id ?? null,
};
}The route names are examples, not claims that one model family is universally best for a task. Replace the mapping only after evaluating your own inputs and success criteria.
Discover model IDs during deployment
export async function validateConfiguredModels() {
const page = await client.models.list();
const available = new Set(page.data.map((item) => item.id));
const required = new Set(Object.values(TASK_ROUTES));
const missing = [...required].filter((id) => !available.has(id));
if (missing.length) {
throw new Error(`Configured model IDs unavailable: ${missing.join(', ')}`);
}
return available;
}Run this at deployment or startup, not before every request. The authenticated list can depend on the key and allowlist. A successful discovery check does not prove output quality or current health.
Compare models without one failure cancelling the batch
Promise.all() rejects immediately when one promise fails. An evaluation harness usually needs every result, so use Promise.allSettled().
import { performance } from 'node:perf_hooks';
async function evaluateOne(model, prompt) {
const started = performance.now();
const response = await client.chat.completions.create({
model,
messages: [
{ role: 'system', content: 'Return billing, technical, or account only.' },
{ role: 'user', content: prompt },
],
max_tokens: 20,
});
return {
model,
latencyMs: Math.round(performance.now() - started),
text: response.choices[0]?.message?.content ?? '',
inputTokens: response.usage?.prompt_tokens ?? null,
outputTokens: response.usage?.completion_tokens ?? null,
requestId: response._request_id ?? null,
};
}
export async function compareModels(prompt) {
const models = Object.values(MODELS);
const settled = await Promise.allSettled(
models.map((model) => evaluateOne(model, prompt)),
);
return settled.map((result, index) =>
result.status === 'fulfilled'
? { ok: true, ...result.value }
: {
ok: false,
model: models[index],
error: result.reason?.name ?? 'UnknownError',
},
);
}This produces integration evidence, not a winner. Score task correctness, formatting, latency, error rate and cost across a representative dataset.
Estimate cost with dated rates
LumeAPI listed these rates per 1 million input/output tokens on July 20, 2026:
| Model | Input | Output | Displayed reference |
|---|---|---|---|
gpt-5.4-mini | $0.225 | $1.35 | $0.75 / $4.50 |
claude-sonnet-4-6 | $1.80 | $9.00 | $3.00 / $15.00 |
gemini-3.5-flash | $0.90 | $5.40 | $1.50 / $9.00 |
The GPT example is 70% below its displayed reference; the Claude and Gemini examples are 40% lower. “Up to 70%” is not a universal discount.
const RATES = Object.freeze({
'gpt-5.4-mini': { input: 0.225, output: 1.35 },
'claude-sonnet-4-6': { input: 1.80, output: 9.00 },
'gemini-3.5-flash': { input: 0.90, output: 5.40 },
});
export function estimateCost(model, inputTokens, outputTokens) {
const rate = RATES[model];
if (!rate) throw new Error(`Missing dated rate for ${model}`);
return (inputTokens * rate.input + outputTokens * rate.output) / 1_000_000;
}Use actual LumeAPI usage records for reconciliation. Local estimates can drift when rates change or when retries and caching affect billed usage.
Isolate errors by model and task
import OpenAI from 'openai';
export async function safeGenerate(task, prompt) {
const model = modelForTask(task);
try {
return await generateForTask(task, prompt);
} catch (error) {
if (error instanceof OpenAI.BadRequestError) {
throw new Error(`Invalid request for ${task}/${model}`);
}
if (error instanceof OpenAI.NotFoundError) {
throw new Error(`Configured model unavailable: ${model}`);
}
if (error instanceof OpenAI.AuthenticationError) {
throw new Error('Check the server-side LumeAPI key and baseURL.');
}
if (error instanceof OpenAI.RateLimitError) {
throw new Error(`Rate limited on ${model}; apply bounded policy.`);
}
throw error;
}
}Do not automatically change models on 400, 401 or a missing model. Fix configuration first. Application-controlled fallback belongs to a separate, tested LLM fallback policy.
Add a redaction-safe application log
export function logResult(task, result, startedAt) {
console.log(JSON.stringify({
task,
model: result.model,
requestId: result.requestId,
latencyMs: Date.now() - startedAt,
inputTokens: result.usage?.prompt_tokens ?? null,
outputTokens: result.usage?.completion_tokens ?? null,
}));
}Do not log keys, full prompts or sensitive model output. Reconcile the request ID, model and timestamp with LumeAPI's per-call logs for cost and latency analysis.
Multi-model architecture boundaries
One OpenAI-compatible text shape reduces integration duplication, but models remain behaviorally different. Retest:
- system/developer instruction handling;
- streaming and disconnect behavior;
- tool-call argument schemas;
- structured output support;
- context/output limits;
- safety responses and refusals;
- task success, latency and cost.
Image and video models share the LumeAPI account and wallet but can use different endpoints, inputs and asynchronous polling. Do not force every modality through a text abstraction.
Why this differs from the basic Node guide
The OpenAI-compatible Node.js guide owns installation, one request and basic streaming. This page owns multi-model policy: validated IDs, task routes, concurrent evaluation, dated cost calculation and failure isolation. Keeping those intents separate avoids a title-only duplicate while giving a production team a complete next step.
Sources and verification
- Official OpenAI Node.js SDK for
baseURL, streaming, request IDs and error classes. - LumeAPI multi-model API, model catalog, and pricing for current IDs, rates, wallet and logging.
- Reddit multi-model gateway discussion and single-endpoint proxy request as community demand signals.
All JavaScript blocks were syntax-checked on July 20, 2026. No customer inference key was used, so the guide does not claim observed output quality, latency or model superiority.