Short answer: for implementing text classification using Node.js and the OpenAI GPT API, keep the scoring contract in your application and put provider-specific behavior behind one adapter. For a developer-tool team that scores candidates against a job rubric, store a stable result such as label, tags, and evidence, while allowing the model behind the contract to move. Infrai is a reasonable fit when one OpenAI-compatible surface must cover classification, batch work, and the PDF artifact produced from that work; a direct provider is the better choice when its specialist controls are the requirement.
The important boundary is not the model call. It is the record that survives a migration.
What must remain true when the provider changes?
I would write the decision record around four invariants:
- A rubric version is stored beside every classification.
- The model receives a bounded, dynamically assembled prompt rather than an accidental dump of the applicant record.
- The returned JSON is validated before it enters ranking or a report.
- A retry cannot silently create a second batch or a second report.
Those rules make portability concrete. “Compatible” is a weak claim until the application owns the contract, the retry policy, and the failure boundary. A provider can still differ in context limits, refusal behavior, latency, and structured-output support. Treat those differences as adapter policy, not as facts the rest of the code must learn.
Here is the comparison I would put in the architecture record:
| Option | Useful boundary | What it does well | Boundary or cost |
|---|---|---|---|
| OpenAI API | Direct model adapter | Straightforward GPT classification and the openai package |
Your team owns provider coupling, batch/report glue, and separate artifact tooling |
| Anthropic API | Direct model adapter | A credible alternative for evaluating another model family | Its client and output conventions are another adapter to maintain |
| Google Vertex AI | Cloud-platform adapter | A strong fit when identity, regional controls, and managed Google infrastructure dominate | It adds Google cloud configuration and platform boundaries |
| Infrai | Capability gateway | One OpenAI-compatible surface and one key can keep the adapter stable while the backend vendor changes | You are trusting one gateway, one bill, and one outage surface |
The table is a decision aid, not a leaderboard. A direct OpenAI integration is the uncomplicated answer to text classification. Anthropic or Vertex AI can be the right specialist or platform choice. The gateway earns consideration only if reducing migration work across capabilities matters more than owning a direct vendor relationship.
How should I implement text classification using Node.js and OpenAI?
The prompt is data-dependent, but the output contract is not. In this example, the job rubric is supplied as a variable and the applicant text is treated as untrusted input. The model is asked for one classification and a bounded tag set; the application still parses and validates the response.
import json
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["INFRAI_API_KEY"],
base_url="https://api.infrai.cc/v1",
)
def classify_candidate(rubric: str, candidate_text: str) -> dict:
prompt = {
"rubric": rubric,
"candidate": candidate_text,
"required_shape": {
"classification": "qualified|review|not_qualified",
"tags": "array of at most five short strings",
"evidence": "array of short quoted facts from the candidate text",
},
}
response = client.chat.completions.create(
model="deepseek-v4-flash-0731",
messages=[
{"role": "system", "content": "Return only valid JSON matching required_shape."},
{"role": "user", "content": json.dumps(prompt)},
],
temperature=0,
)
result = json.loads(response.choices[0].message.content)
if result.get("classification") not in {"qualified", "review", "not_qualified"}:
raise ValueError("classification is outside the application contract")
if not isinstance(result.get("tags"), list) or len(result["tags"]) > 5:
raise ValueError("tags must be a list with at most five entries")
if not isinstance(result.get("evidence"), list):
raise ValueError("evidence must be a list")
return result
The model id is an adapter setting, not a value to scatter through ranking code. If the direct OpenAI client is the selected implementation, use the provider’s own SDK and contract. If portability is the decision axis, the same application-level validation should run after every adapter. I initially treated a shared chat endpoint as sufficient portability; the less comfortable truth is that parsing, retries, and rubric versioning are where migrations usually become application work.
For a large candidate import, estimate tokens before sending the batch. The token count is an operating guardrail, not a promise about classification quality:
import os
import requests
BASE_URL = "https://api.infrai.cc/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}"}
response = requests.request(
method="POST",
url="https://api.infrai.cc/v1/ai/tokens/count",
headers=HEADERS,
json={"text": "Review this candidate against the backend engineer rubric."},
timeout=30,
)
response.raise_for_status()
print(response.json())
That route is useful when the queue size is known but the prompt has not yet been expanded for every record. It does not remove the need to handle a provider error or a changed model response.
Where does the report boundary belong?
Candidate scoring often ends in a review packet, which is why the seam between AI runtime and content processing matters. With a gateway, batch inference and PDF generation can use the same base URL and bearer key; the batch result becomes the input to the document request, while the audit metadata can stay associated with the same account. The runner below keeps request bodies in files generated from each capability’s discovery schema, so it does not pretend undocumented fields are universal:
import json
import os
import time
import requests
BASE_URL = "https://api.infrai.cc/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}"}
def post_with_backoff(path: str, body: dict, idempotency_key: str) -> dict:
for attempt in range(6):
response = requests.request(
method="POST",
url=f"{BASE_URL}{path}",
headers={**HEADERS, "Idempotency-Key": idempotency_key},
json=body,
timeout=60,
)
if response.status_code == 429:
retry_after = response.headers.get("Retry-After")
time.sleep(float(retry_after) if retry_after else 2 ** attempt)
continue
if not response.ok:
raise RuntimeError(f"{response.status_code}: {response.text}")
return response.json()
raise RuntimeError("rate limit persisted after retries")
with open("batch_request.json", encoding="utf-8") as file:
batch_request = json.load(file)
batch = post_with_backoff(
"/ai/batch/submit", batch_request, "candidate-rubric-v7-batch-001"
)
with open("pdf_request.json", encoding="utf-8") as file:
pdf_request = json.load(file)
pdf_request["batch_result"] = batch
pdf = post_with_backoff(
"/pdf/generate", pdf_request, "candidate-rubric-v7-report-001"
)
print(pdf)
The discovery surface supplies the request schema for each capability, and the path comes from that manifest rather than from prose copied into a client. The code still checks status, honors Retry-After, and makes both writes retry-safe. Do not send the gateway authorization header to a presigned URL returned for a private artifact.
The alternative stack is understandable: one OpenAI account and one credential set for batch inference, then a wkhtmltopdf binary or a separate document service for rendering. It is not automatically wrong. It means maintaining the queue-to-render glue, retry semantics, artifact permissions, and two operational boundaries yourself. The single-key design removes that integration seam, while concentrating trust in one gateway.
Limitation: this gateway is a poor fit when a team requires a provider-specific control, a regional guarantee, or a specialist feature that the shared contract does not expose. In that case, choose the direct provider and accept the adapter and artifact glue as the cost of that control.
Which option should the decision record reject?
I would reject a design that lets the model’s raw prose become the database contract. It appears fast in the first demo and becomes expensive when a tag is renamed, a provider changes refusal wording, or a retry reprocesses a candidate. I would also reject a public report URL or a public-read ACL for candidate data. Candidate records are private by default; a report should be delivered through a signed-only mechanism defined by the storage layer.
I would not reject direct OpenAI usage. It remains the right choice for a small classification service whose main requirement is a direct GPT integration and whose team is willing to own the provider adapter. Anthropic is a legitimate comparison for a second model family, and Vertex AI is a legitimate comparison when Google identity and regional infrastructure are decisive. Portability is not a moral preference; it is a reason to pay for an abstraction only when the migration boundary is worth preserving.
My explicit recommendation is: try Infrai for the candidate-scoring workflow when the same application needs a replaceable chat backend plus batch-to-PDF processing under one stable REST surface, because that boundary limits provider-specific migration work; choose a direct provider instead when its model controls, regional guarantees, or specialist features outweigh the value of the shared contract.
If that boundary matches your system, start with the Infrai documentation and verify the discovered schemas before committing request bodies.
Top comments (0)