Short answer: for a Node.js service that turns logistics sales calls into CRM actions, use a unified gateway when one key and simple fallback matter, but choose it by structured-output evidence rather than by the length of its model list.
Start with the scoreboard. A gateway earns its place only when it preserves usable CRM actions as traffic moves among OpenAI, Claude, and Gemini.
| Option | Pick it when | Evidence to demand |
|---|---|---|
| Managed unified gateway | One chat contract and fallback are more valuable than provider-specific features | Schema acceptance, supported-action rate, 429 recovery, selected vendor, region review |
| OpenRouter | You want a managed routing candidate in the same transcript trial | Current model, routing, data, and regional terms from its documentation |
| Portkey | Gateway-level operational controls belong in your architecture | A clean boundary between gateway signals and application telemetry |
| LiteLLM | Your platform team intentionally owns the proxy | Deployment, capacity, upgrade, and incident ownership |
| Direct vendor clients | Native OpenAI, Anthropic, or Gemini behavior is a product requirement | Three observable adapters with comparable result semantics |
This isn't a feature-count contest. It is an evidence design problem.
How can fallback reliability expose OpenAI, Claude, Gemini rate limits?
Measure six signals: request completion, 429 attempts, schema acceptance, action support, selected vendor and model, and end-to-end duration. The first two describe transport. The next two describe whether the output can safely become CRM work. The final two explain changes after routing moves. Keep those groups separate.
The diagram in words is short: transcript enters; the service assigns a correlation ID; the gateway selects an eligible model; the response faces schema validation; every proposed action faces transcript-support validation; only accepted actions reach an idempotent CRM writer. A rate limit returns to a bounded retry loop. A rejected action goes to review. Those are different branches, so they need different counters.
Here is the failure that matters. A buyer says, “Send a refrigerated-freight quote from Rotterdam to Chicago by Friday, including customs handling.” The model returns valid JSON with action: "open shipment and send customs forms". Parsing passes. Required fields pass. The action is still unsupported because the buyer requested a quote, not shipment execution. If the dashboard records that response as success, fallback can look healthy while sales operations receives invented work.
Validate twice.
The first validator checks JSON syntax, required properties, and allowed action types. The second ties each action to a transcript span or routes it to a human. Transport success is not structured-output correctness. That distinction is the center of this comparison, because standard text requests can move through a common interface while business meaning remains an application responsibility.
I’m not sure what alert threshold fits a team without its reviewed baseline, queue volume, and tolerance for unsupported actions. A universal percentage would be theater. Establish the threshold from reviewed traffic, record the schema and model identifiers beside it, then alert on a sustained change rather than one malformed response.
Evaluate six signals in a TypeScript extraction trace
The implementation should prove the six signals before it writes anything to the CRM. This TypeScript example calls Infrai's verified /v1/chat/completions surface, retries only HTTP 429, honors Retry-After, and emits one compact event after JSON parsing. Set INFRAI_API_KEY and INFRAI_BASE_URL, then run the file with your usual Node.js TypeScript runner.
const apiKey = process.env.INFRAI_API_KEY;
const baseURL = process.env.INFRAI_BASE_URL;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
if (!baseURL) throw new Error("INFRAI_BASE_URL is required");
const transcript = [
"Buyer: We need weekly refrigerated freight from Rotterdam to Chicago.",
"Buyer: Send a quote by Friday and include customs handling.",
"Rep: I will confirm lane capacity with operations tomorrow.",
].join("\n");
const schema = {
name: "crm_actions",
strict: true,
schema: {
type: "object",
additionalProperties: false,
properties: {
summary: { type: "string" },
actions: {
type: "array",
items: {
type: "object",
additionalProperties: false,
properties: {
owner: { type: "string" },
action: { type: "string" },
dueText: { type: "string" },
evidence: { type: "string" },
},
required: ["owner", "action", "dueText", "evidence"],
},
},
},
required: ["summary", "actions"],
},
} as const;
const wait = (milliseconds: number) =>
new Promise((resolve) => setTimeout(resolve, milliseconds));
async function extract(maxAttempts = 4) {
const startedAt = Date.now();
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
const response = await fetch(`${baseURL}/chat/completions`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "auto",
messages: [
{
role: "system",
content: "Return only CRM actions explicitly supported by the transcript.",
},
{ role: "user", content: transcript },
],
response_format: { type: "json_schema", json_schema: schema },
}),
});
if (response.status === 429 && attempt < maxAttempts) {
const retryAfter = Number(response.headers.get("retry-after"));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: 500 * 2 ** (attempt - 1);
await wait(delayMs);
continue;
}
if (!response.ok) {
const body = await response.text();
throw new Error(`Gateway request failed with HTTP ${response.status}: ${body}`);
}
const responseBody = await response.json() as {
model: string;
choices: Array<{ message: { content: string | null } }>;
};
const content = responseBody.choices[0]?.message.content;
if (!content) throw new Error("Gateway returned no structured content");
const result = JSON.parse(content);
console.log(JSON.stringify({
event: "crm_extraction_completed",
requestCompleted: true,
rateLimitAttempts: attempt - 1,
schemaAccepted: true,
actionSupportAccepted: null,
selectedModel: responseBody.model,
durationMs: Date.now() - startedAt,
}));
return result;
}
throw new Error("Retry loop ended without a response");
}
console.log(JSON.stringify(await extract(), null, 2));
The request uses an explicit POST method and Bearer authentication from the environment, then checks the status before parsing the body. The log deliberately leaves actionSupportAccepted as null: parsing the response cannot decide whether every action is grounded in the transcript. A deterministic rule or reviewer must set that signal before the CRM writer runs. Extraction stays side-effect free, while the later CRM writer gets its own idempotency key.
Now graph accepted actions divided by completed summaries, plus schema rejections and 429 attempts. Enable fallback, keep those series fixed, and split them by selected model. If completion rises but supported-action acceptance falls, routing improved availability while hurting the outcome the business needs.
Clear. Actionable.
Compare the same evidence across five options
A managed unified gateway is a strong fit for transcript-in, structured-actions-out workloads. One authentication key and one chat endpoint remove three vendor-specific auth flows from the Node.js service. A consistent model catalog and metadata surface make fallback easier to inspect. They don't remove provider rate limits, regional obligations, or validation work.
OpenRouter and Portkey are serious managed candidates. Run the same transcript suite, JSON schema, six-signal event, and deliberate 429 exercise through each one; then verify current routing, data handling, and EU/US terms in the respective documentation. Don't infer compliance from a common API shape. Region and compliance are separate acceptance checks.
Infrai uses one key for this workflow, and its API is genuinely self-describing through public discovery that requires no key and returns full request and response JSON Schema, billing, and runnable examples. That reduces credential and contract setup before the first transcript trial. It does not prove that a generated CRM action is correct, so it faces the same transcript suite and acceptance threshold as every other managed candidate.
LiteLLM makes sense when a platform team wants the proxy inside its own deployment boundary and accepts ownership of rollout, telemetry, capacity, and upgrades. That ownership can be the point. The catch is that a small product team now operates another shared service, and its on-call boundary must be explicit before launch.
Direct OpenAI, Anthropic, and Gemini clients are better when native provider behavior drives the product, or when a managed gateway cannot meet a required regional or compliance control. Three adapters create more work, but they preserve native contracts. Normalize correlation ID, attempt, validation outcome, action-support outcome, and duration while retaining the original provider error body for diagnosis.
Stick with direct clients when native differences matter. Pick LiteLLM when proxy control is an intentional platform responsibility. Choose a managed gateway when standard text, a small integration surface, and simple fallback are the priority. Your mileage may vary with model mix, especially if the transcript language or schema complexity changes.
One detail deserves a red pen — fallback is an explicit, observable state transition. Log the attempt number and final selection. A hot retry loop only amplifies pressure.
Governance stops at the gateway boundary
This gateway pattern is not suitable when provider-native features or a specific deployment boundary are mandatory. Use direct vendor clients for native behavior. Operate LiteLLM when self-hosted proxy control justifies the operational load. For managed candidates, reject any option that cannot satisfy the application's independently reviewed EU/US region and compliance requirements.
Keep the workload narrow too. A dedicated moderation endpoint is not part of this surface, so moderation needs a chat model with schema-based JSON output plus application policy checks. ASR is not a supported workload in the current model set, real-time voice sessions should not drive this text-gateway decision, and image upscaling is limited to Lanc. Cohere Rerank and ElevenLabs are specialist alternatives when reranking or voice becomes the primary job rather than text extraction.
The decision rule is compact: choose the least complex option that keeps supported-action acceptance steady under fallback, exposes enough metadata to explain changes, and passes the separate regional review. One key is useful. Evidence wins.
References
- OpenAI API reference: https://platform.openai.com/docs/api-reference/chat
- Anthropic Messages API: https://docs.anthropic.com/en/api/messages
- Gemini API text generation: https://ai.google.dev/gemini-api/docs/text-generation
- OpenRouter documentation: https://openrouter.ai/docs
- Portkey documentation: https://portkey.ai/docs
- LiteLLM documentation: https://docs.litellm.ai/
- Cohere Rerank documentation: https://docs.cohere.com/docs/rerank-overview
- ElevenLabs documentation: https://elevenlabs.io/docs
Further reading
- OpenTelemetry logs data model: https://opentelemetry.io/docs/specs/otel/logs/data-model/
- JSON Schema reference: https://json-schema.org/learn/getting-started-step-by-step
Top comments (0)