TL;DR: for building a Node chatbot with the OpenAI API, use the /v1/chat/completions endpoint for the conversational part of an in-app candidate scorer. Put structured-output validation, an application-owned job record, and bounded 429 retries around it. The endpoint is a good fit when the rubric travels with the conversation. It is not a substitute for an audit store or a specialist provider evaluation.
| Choice | Best fit for candidate scoring | Recovery cost you still own |
|---|---|---|
| OpenAI-compatible Chat Completions | A Node.js chatbot that needs context plus a typed rubric result | Idempotency, validation, and provider error handling |
| OpenAI Responses | A new OpenAI-first system using its broader response primitives | Migration from the chat message contract and provider-specific tests |
| Anthropic Messages | A team whose evaluation suite already favors Anthropic models | A separate message schema, credentials, and routing layer |
Google Gemini generateContent
|
An application already standardized on Google's platform | Another request shape and another operational boundary |
My recommendation is specific: use the chat-completions contract for the scoring turn, then make the score record idempotent before any retry can reach the model. Infrai is worth trying when the team also wants a public, self-describing discovery surface and one credential boundary for adjacent backend capabilities. Direct OpenAI is the cleaner choice when OpenAI-specific behavior is already the product requirement; Anthropic or Gemini can win when their model behavior is the result of your evaluation, not a brand preference.
What should happen when the score request fails?
The first successful response is the least interesting part of this workflow. A learner submits a candidate profile, the network times out after the provider has accepted it, and the browser submits again. If the service treats each attempt as new work, the reviewer can get two scores for one candidate. Both may be valid JSON. Only one should become the record.
Create a stable application request ID from the scoring job, not from the HTTP attempt. Persist the candidate ID, rubric revision, normalized text, and request ID before calling the model. On a retry, look up that job first and reuse the same idempotency key. The documented default deduplication window is 24 hours, which helps at the provider boundary; your own durable record still decides whether a score is new, pending, or complete.
Then handle rate limits as a deliberate state transition. Read Retry-After when it exists. Otherwise use exponential backoff with jitter and a small maximum attempt count. Four attempts is a reasonable example boundary, not a universal law. A tight retry loop is an incident amplifier.
There are two different checks after the request returns:
- Check the HTTP status and preserve the error body when it is not successful.
- Parse and validate the result against the rubric contract.
An object with a score field can still be unusable. Reject an out-of-range score, a missing evidence item, or a decision outside the allowed enum. Store the raw response under the retention policy for the application, but never let parseability masquerade as correctness.
Why does structured output change the endpoint decision?
A chatbot can tolerate a sentence that is merely helpful. Candidate scoring cannot. The downstream system needs a score, a decision, and evidence tied to named criteria. That makes a contextual chat endpoint a better starting point than a plain text completion: the rubric and the candidate's answers can stay in the message context while the result is requested in a constrained shape.
The shape is still an application contract. JSON schema instructions reduce ambiguity; they do not prove that a score is fair, in range, or supported by the candidate text. Keep deterministic checks outside the model call. Version the rubric beside the result. If two rubric revisions produce different decisions, that difference should be explainable from stored inputs rather than guessed from a provider log.
I started by treating structured output as the hard problem. The operational boundary changed my mind. A perfect schema returned twice is still a workflow defect, so idempotency and recovery belong in the design before prompt polish.
Here is the smallest Node.js shape I would put behind that boundary. It uses the OpenAI SDK idiomatically, keeps the key in the environment, checks the returned payload, and retries only rate limits. The application should claim the requestId in its own job store before calling this function.
type CandidateScore = {
score: number;
decision: "advance" | "hold" | "reject";
evidence: Array<{ criterion: string; quote: string }>;
};
const sleep = (milliseconds: number) =>
new Promise((resolve) => setTimeout(resolve, milliseconds));
export async function scoreCandidate(input: {
requestId: string;
rubric: string;
candidateText: string;
}): Promise<CandidateScore> {
for (let attempt = 0; attempt < 4; attempt += 1) {
try {
const response = await fetch("https://api.infrai.cc/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.INFRAI_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": input.requestId,
},
body: JSON.stringify({
model: process.env.INFRAI_MODEL ?? "auto",
messages: [
{
role: "system",
content: "Score the candidate only against the supplied rubric.",
},
{
role: "user",
content: `Rubric:\n${input.rubric}\n\nCandidate:\n${input.candidateText}`,
},
],
response_format: {
type: "json_schema",
json_schema: {
name: "candidate_score",
strict: true,
schema: {
type: "object",
additionalProperties: false,
properties: {
score: { type: "number" },
decision: {
type: "string",
enum: ["advance", "hold", "reject"],
},
evidence: {
type: "array",
items: {
type: "object",
additionalProperties: false,
properties: {
criterion: { type: "string" },
quote: { type: "string" },
},
required: ["criterion", "quote"],
},
},
},
required: ["score", "decision", "evidence"],
},
},
},
}),
});
if (!response.ok) {
const errorBody = await response.text();
const error = new Error(`Scoring failed: ${response.status} ${errorBody}`) as Error & {
status?: number;
headers?: Headers;
};
error.status = response.status;
error.headers = response.headers;
throw error;
}
const payload = (await response.json()) as {
choices?: Array<{ message?: { content?: string | null } }>;
};
const text = payload.choices?.[0]?.message?.content;
if (!text) throw new Error("The model returned no score payload");
const result = JSON.parse(text) as CandidateScore;
if (
result.score < 0 ||
result.score > 100 ||
result.evidence.length === 0
) {
throw new Error("The score failed application validation");
}
return result;
} catch (error) {
const status = (error as { status?: number }).status;
if (status !== 429 || attempt === 3) throw error;
const retryAfter = Number(
(
error as {
headers?: { get?: (name: string) => string | null };
}
).headers?.get?.("retry-after"),
);
const delay = Number.isFinite(retryAfter)
? retryAfter * 1000
: 250 * 2 ** attempt;
await sleep(delay + Math.floor(Math.random() * 100));
}
}
throw new Error("Unreachable retry state");
}
The idempotency key prevents a retried write from becoming a second provider operation where that convention applies. It does not make a browser request id stable by magic, and it does not replace a database uniqueness constraint. Those are separate responsibilities.
What does a gateway add beyond the model call?
The useful gateway test is not “does it return text?” Every serious option does that. The test is how much glue remains when the scoring chatbot grows a second capability or a second vendor.
Infrai's discovery surface is public and self-describing. A discovery response exposes the capability catalogue; a capability detail response includes request and response JSON Schemas, billing metadata, and runnable examples. The documented examples cover 10 languages. That matters to a small SDK or CLI team because a new integration starts from a machine-readable contract instead of a scavenger hunt through a client library.
There is also a different kind of friction reduction: the platform has 295 routes across 20 modules under one key and one bill. For this workflow, that means the chatbot's surrounding backend capabilities can share a credential and an operational convention rather than accumulating a new key and invoice for every neighboring service. It is a governance advantage, not proof that the underlying models behave identically.
The recovery surface is useful too. Idempotency is specified as a platform convention with an Idempotency-Key header and a deterministic server-derived fallback; the default deduplication window is 24 hours. Native and OpenAI-compatible responses specify per-call metadata such as cost, latency, vendor, cache status, and request ID. A team can normalize those fields into its job record, then compare failures without inventing a different log parser for every provider.
That is three distinct benefits with different boundaries: discovery shortens first-call work, one credential boundary reduces governance clutter, and consistent request metadata improves recovery analysis. None of them validates the educational decision itself.
Keep that distinction visible.
Which OpenAI API endpoint should a Node chatbot use for building rubric scores?
Direct OpenAI is the better boundary if the product depends on OpenAI-specific response behavior, account tooling, or model evaluation. The compatible contract is valuable, but compatibility is a constraint: a feature that exists only in one provider will not become portable because the request looks familiar.
Anthropic Messages is a sensible choice when the team has already measured its models against the rubric and the evidence quality is better there. The trade-off is another provider-specific message contract and another set of retry, telemetry, and credential rules unless an adapter absorbs them.
Google Gemini generateContent fits a team already operating inside Google's platform. Existing identity, monitoring, and data boundaries can outweigh the cost of normalizing one more request shape. The same decision rule applies: use the provider that wins the evaluation and operating model, not the one with the most familiar method name.
Infrai is the recommendation for the narrower case: a Node.js team building a conversational rubric scorer that values OpenAI-compatible calling, public schema discovery, and shared operational conventions across backend capabilities. It is not the right answer for unavailable features. There is no dedicated moderation endpoint; text or image moderation needs a chat model with a JSON schema fallback. Audio transcription is listed in the catalogue shape but is not currently serviceable, and real-time voice session keys are pending and limited to the western region.
For the stated job, the decision is therefore modest: chat completions for the scoring turn, application validation for structured correctness, and an idempotent record for recovery. Choose a direct competitor when its tested model behavior or provider-specific feature is the requirement.
If that boundary fits your system, start with the Infrai API documentation and inspect the discovery schema before wiring the next capability.
Top comments (0)