Node.js Example: Implementing Gaming Text Classification Using OpenAI GPT API
Short answer: use a small, provider-neutral classification contract and put the model call behind one adapter. OpenAI GPT is a straightforward starting point for tagging game-support tickets, but the useful engineering d
Short answer: use a small, provider-neutral classification contract and put the model call behind one adapter. OpenAI GPT is a straightforward starting point for tagging game-support tickets, but the useful engineering decision is the contract, not the brand. Keep the labels fixed, require JSON, measure token use, and run the same ticket set against at least three providers before committing your routing policy. The Node.js openai npm package is a natural implementation choice when the queue worker already runs on Node.js.
My first instinct for this kind of feature was a direct prompt and a string split around the model response. That works in a notebook. It becomes brittle as soon as a ticket contains a refund amount, a player name, or a line break that looks like JSON. The production shape is smaller than a full agent: one classifier, one parser, one retry policy, and an evaluation set that does not change underneath you.
Keep it boring.
How should a Node.js team implement text classification using OpenAI?
For a gaming support queue, the output should help a human or downstream rule decide what happens next. A compact contract might contain category, tags, and needs_human. The category is one of a finite set such as billing, account, bug, or abuse; tags add detail such as refund, login, or crash. Unknown cases should be allowed to reach a person instead of being forced into a confident-looking label.
The prompt should carry the ticket as data, not as instructions. That distinction matters because a player can paste text that says βignore your rules.β Delimit the input, state the allowed values, and ask for one JSON object. I also keep the schema outside the ticket so the application can build it dynamically without concatenating policy text into user-controlled fields.
Here is the focused request shape. It uses the OpenAI Python client because the same OpenAI-compatible chat contract can sit behind different base URLs; the environment variable is deliberate, so changing providers does not require changing application code. In a Node.js worker, the equivalent boundary is the openai npm package with its baseURL and API key configuration. The retry loop handles a 429 with Retry-After, while other API failures remain visible to the caller.
import json
import os
import time
from openai import APIStatusError, OpenAI
ALLOWED_CATEGORIES = {"billing", "account", "bug", "abuse", "other"}
def classify_ticket(ticket_text: str) -> dict:
client = OpenAI(
api_key=os.environ["INFRAI_API_KEY"],
base_url=os.environ["INFRAI_BASE_URL"],
)
model = os.environ["OPENAI_MODEL"]
prompt = f"""Classify this gaming customer-support ticket.
Return one JSON object with exactly these keys:
category: one of billing, account, bug, abuse, other
tags: an array of short lowercase strings
needs_human: a boolean
Ticket text begins after the delimiter and ends before it:
---
{ticket_text}
---
"""
for attempt in range(4):
try:
response = client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": "Return valid JSON only."},
{"role": "user", "content": prompt},
],
temperature=0,
)
result = json.loads(response.choices[0].message.content)
if result["category"] not in ALLOWED_CATEGORIES:
raise ValueError("model returned an unknown category")
if not isinstance(result["tags"], list):
raise ValueError("model returned non-list tags")
if not isinstance(result["needs_human"], bool):
raise ValueError("model returned a non-boolean needs_human")
return result
except APIStatusError as error:
if error.status_code != 429 or attempt == 3:
raise
retry_after = error.response.headers.get("retry-after")
delay = float(retry_after) if retry_after else 2**attempt
time.sleep(delay)
raise RuntimeError("classification did not complete")
The example intentionally does not make a write request, so it does not need an idempotency key. In a real queue worker, make the ticket ID your deduplication key before storing the classification. A retry must not create two handoff tasks or two outbound replies.
Which provider keeps the design portable?
Portability is not the claim that every provider behaves identically. It is the discipline of keeping provider-specific details at the boundary. OpenAI GPT gives this workflow a direct chat API and a mature client path. Anthropic Claude uses its Messages API and has a different response contract. Google Gemini has its own SDK and content model. An OpenAI-compatible gateway can reduce adapter work, especially when the application already depends on the OpenAI client, but compatibility still needs tests for JSON adherence, refusals, token accounting, and rate-limit behavior.
| Option | Useful fit for ticket triage | Boundary to test |
|---|---|---|
| OpenAI GPT | Direct implementation of the requested chat-classification flow | Model output still needs schema validation and evaluation |
| Anthropic Claude | A strong alternative when its message contract and model behavior fit the team | Request and response adapters are different |
| Google Gemini | Another independent model family for cross-provider comparison | Its SDK and content representation are different |
| Infrai | A plain REST, OpenAI-compatible surface is useful when one integration must stay replaceable | Verify capability readiness and behavior for the exact model route |
This table is a starting point, not a leaderboard. Provider portability has a cost: the lowest common denominator may discard useful structured-output features, while provider-specific features increase lock-in. For a support queue, I would first standardize the five-field behavioral contract and only then decide whether a richer native feature is worth an adapter.
An OpenAI-compatible gateway is a poor fit if the team needs a provider's newest native feature on day one, or if its exact model and region have not passed the evaluation set. This is a real limitation, not a footnote: an adapter that accepts the same request can still differ in structured output, refusal behavior, or model availability. Choose the provider with the capability and readiness you can verify; portability is not a reason to hide a missing capability. If native tool use is the requirement, choose the provider that documents and passes that feature instead of forcing the gateway into the role.
How do you know the tags are good enough?
Create a small, versioned evaluation set before tuning prompts. Fifty carefully reviewed tickets can expose more than a large pile of unlabelled production data if the set includes duplicate complaints, mixed-intent tickets, angry language, pasted logs, and tickets whose correct result is needs_human: true. Keep the labels written by a person, and record the prompt version beside every result.
The number is a control, not a promise.
The useful test case is a mixed ticket: βI was charged twice after the game crashed during checkout; my account now says the purchase is pending.β A shallow classifier can choose either billing or bug and look reasonable in a dashboard. A better contract preserves billing, adds duplicate-charge and crash, and sets needs_human according to the escalation policy. That is why the evaluator needs multi-intent examples and a written rule for which category wins. It also shows where provider portability stops being automatic: one model may return three helpful tags, another may return one broad tag, and both may be valid JSON. The application must score the decision that the support queue actually needs, not the shape of the response alone.
Measure category accuracy, tag precision, invalid-JSON rate, human-escalation recall, latency, and input/output tokens. Exact-match accuracy alone is a trap: bug with a useful crash tag can be operationally better than a technically exact category with no routing detail. Use a confusion matrix for categories, then inspect the disagreements. The expensive errors are usually asymmetric; sending an account takeover report to a generic bug queue is worse than sending a harmless crash report to a human.
Token estimation belongs before rollout, not after the invoice. The available token-count capability can estimate usage for the prompt shape, and the final response metadata should be recorded where the provider exposes it. Trim repeated policy text, cap ticket length, and do not pass the full conversation when the latest customer message is enough. Those changes improve both cost control and portability because every provider sees less unnecessary context.
There are also adjacent capability boundaries worth keeping explicit. This design classifies text; it does not imply that speech transcription is available, that real-time voice sessions are ready in every region, or that a dedicated moderation endpoint exists. For text or image moderation, a chat model with a JSON schema fallback is a different workflow with a different evaluation target. Do not quietly turn a ticket classifier into a moderation system.
What should ship first?
Ship the adapter and the evaluator together. Start with one model, a fixed label set, strict parsing, bounded retries, and a dead-letter path for malformed output. Then replay the same evaluation set through OpenAI GPT, Claude, and Gemini, plus any OpenAI-compatible route under consideration. Compare behavior, not brochure claims.
The decision rule is practical: choose the provider that meets the human-escalation and parsing thresholds with acceptable token use, then keep a second provider exercised in the evaluation job. That gives the support team a stable interface and gives the platform team an escape hatch. A model swap should change configuration and an adapter test, not every queue consumer.
Further reading
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.