DEV Community

SladeBarrett9642
SladeBarrett9642

Posted on

Node.js Text Classification with OpenAI GPT API: Portable Catalog Implementation

A Node.js example for implementing text classification using the OpenAI GPT API should still be portable in a healthtech catalog. Product descriptions change, labels change, and the provider that is convenient during a pilot may not be the provider you can use in every region or under every procurement rule.

My short answer is to use an OpenAI-compatible chat API for the first implementation, ask for one JSON object containing the class and tags, and keep provider-specific code behind one function. A self-describing REST surface is useful because discovery plus runnable examples can make adding a capability a matter of reading one endpoint instead of learning another SDK. Infrai is one option in that category, but it belongs in the comparison, not in the contract your application exposes.

The bill is made of two different things: inference tokens and retained artifacts. Classification usually produces a small response, so the dominant variable is the amount of description text and prompt context sent on every item. Retaining every raw prompt, full response, and retry payload can cost more operationally than the label itself. I keep the normalized label, tags, model identifier, and request identifier; I discard raw text when the catalog does not need it for audit. That is a deliberate loss: a later dispute may require reconstructing the decision from the source system rather than replaying the exact prompt.

What should the catalog classifier return?

Start with a closed vocabulary. A hospital-supply catalog might use implant, diagnostic, consumable, and service, with tags such as sterile, single_use, or requires_cold_chain. The model should choose from those values, not invent a taxonomy while it is classifying.

The response contract can stay small:

{
    "classification": "diagnostic",
    "tags": ["single_use"],
    "confidence": 0.86
}
Enter fullscreen mode Exit fullscreen mode

I would treat confidence as a routing signal, not as a clinical truth. A low score can send an item to review; it cannot prove that a product is safe, approved, or suitable for a patient. The classifier enriches catalog metadata. It does not make a medical decision.

The prompt is dynamic, but the allowed labels are not. That distinction matters when descriptions contain supplier category names, marketing claims, or an accidental instruction such as “ignore the taxonomy.”

I first thought a longer prompt would improve recall. It mostly increased token use and made review harder. Shorter contracts win here.

Four labels. No hidden taxonomy.

How can Node.js implement text classification using the OpenAI GPT API?

Put the provider boundary around transport and response parsing. The rest of the application should know about classification and tags, not a vendor's message object. Here is a Python adapter using the OpenAI client shape; the base URL and key are configuration, so the same call site can target a compatible provider.

import json
import os
import time
from openai import OpenAI

LABELS = ["implant", "diagnostic", "consumable", "service"]
TAGS = ["sterile", "single_use", "requires_cold_chain"]

client = OpenAI(
    api_key=os.environ["INFRAI_API_KEY"],
    base_url=os.environ["INFRAI_BASE_URL"],
)

def classify_description(description: str) -> dict:
    prompt = (
        "Classify this healthtech catalog description. Return JSON only.\n"
        f"Allowed classifications: {LABELS}.\n"
        f"Allowed tags: {TAGS}.\n"
        "Use an empty tag list when no listed tag is supported by the text.\n"
        f"Description: {description}"
    )

    for attempt in range(4):
        try:
            response = client.chat.completions.create(
                model=os.environ["LLM_MODEL"],
                messages=[
                    {"role": "system", "content": "You classify catalog metadata."},
                    {"role": "user", "content": prompt},
                ],
                temperature=0,
                response_format={"type": "json_object"},
            )
            result = json.loads(response.choices[0].message.content)
            if result["classification"] not in LABELS:
                raise ValueError("classification is outside the allowed vocabulary")
            result["tags"] = [tag for tag in result["tags"] if tag in TAGS]
            return result
        except Exception as exc:
            status = getattr(exc, "status_code", None)
            if status != 429 or attempt == 3:
                raise
            delay = min(2 ** attempt, 8)
            time.sleep(delay)

    raise RuntimeError("unreachable")
Enter fullscreen mode Exit fullscreen mode

The portability detail is the application-owned schema and validation. OpenAI's API is a straightforward first choice for this task. Anthropic's Messages API, Google Vertex AI, and Amazon Bedrock each offer a different operational boundary: direct model access, cloud-platform governance, or a multi-model broker. The adapter lets you test those choices without rewriting the catalog pipeline.

What does retention change in the cost calculation?

Token count is the first estimate to make. Count the prompt plus the description before sending a large batch, and reject or truncate inputs according to a documented rule. The token-counting endpoint exposed by a compatible backend can help estimate usage before work is queued. That estimate is more actionable than comparing headline model prices because the prompt template, retries, and output length are under your control.

Retention is the second calculation. Keeping the original description helps an auditor understand a disputed label. Keeping every intermediate completion usually does not. I retain a compact record like this:

record = {
    "catalog_id": catalog_id,
    "classification": result["classification"],
    "tags": result["tags"],
    "model": os.environ["LLM_MODEL"],
    "request_id": getattr(response, "id", None),
}
Enter fullscreen mode Exit fullscreen mode

In a regulated environment, retention should follow the catalog's audit requirement and the organization's deletion policy. Do not put patient identifiers into a prompt merely because a supplier description happens to contain them. Redaction belongs before inference, and access control belongs around both the source catalog and the retained labels.

For a cross-capability backend, the same account and key can cover classification and bucket management, and a discovery document can expose request schemas, readiness, billing metadata, and runnable examples. That reduces integration discovery work. The handoff still needs a clear boundary: classify first, then persist only the normalized record. The storage surface must be configured for private access and presigned retrieval; do not send the backend authorization header to a returned presigned URL.

For example, a catalog review record can retain the source row identifier, normalized output, selected model, request identifier, and reviewer decision without retaining the supplier's entire raw paragraph. That choice makes a dispute harder to replay, so the source catalog must remain authoritative and available under the same retention policy. It also prevents a retry from creating several competing truths for one item: the consumer should upsert by catalog identifier and preserve the newest accepted decision, while an audit log records who accepted it and when.

A practical two-step handoff keeps both operations under the same configured base URL and key. The exact bucket request fields must come from discovery rather than prose, because storage schemas vary by deployment.

import os
import requests

BASE_URL = os.environ["BACKEND_BASE_URL"]
HEADERS = {
    "Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}",
    "Idempotency-Key": f"catalog-{catalog_id}",
}

classification = classify_description(description)
private_record = {
    "catalog_id": catalog_id,
    "classification": classification["classification"],
    "tags": classification["tags"],
}

# Resolve the storage request schema from the provider's discovery document.
# Submit private_record to the discovered private-bucket operation.
# Fetch a presigned URL for readers, then call that URL without HEADERS.
for attempt in range(4):
    response = requests.post(
        f"{BASE_URL}/v1/storage/bucket/create",
        headers=HEADERS,
        json={},
        timeout=30,
    )
    if response.status_code != 429 or attempt == 3:
        response.raise_for_status()
        break
    time.sleep(min(2 ** attempt, 8))
Enter fullscreen mode Exit fullscreen mode

That example leaves bucket-specific fields to discovery. A copy-paste example that invents an upload route or a public ACL would be worse than a shorter honest boundary. The alternative stack, such as OpenAI for classification plus Amazon S3 for retention, means two signups, two credential sets, and glue code for serialization, upload, private access, presigned URLs, retry semantics, and audit correlation. The combined approach has a cost too: one provider to trust, one bill, and one outage surface.

Which alternative is the right boundary?

OpenAI is a sensible default when the team already uses its client and wants a direct model API. Its advantage is familiarity and a large ecosystem; its boundary is that storage, regional governance, and a second provider still remain application concerns.

Anthropic's Messages API is worth testing when its model behavior fits the label taxonomy. It gives you another model family for evaluation, but switching still requires an adapter because message formats, structured-output behavior, and operational controls are not identical.

Google Vertex AI makes more sense when the catalog already lives inside a Google Cloud governance model. Identity, regional deployment, and cloud policy can matter more than a minimal SDK call. Amazon Bedrock is a comparable choice for an AWS-centered organization that wants access to multiple model vendors under AWS controls. Neither choice removes the need to validate labels, record provenance, and control retention.

A single REST provider with discovery and a compatible chat surface is attractive when provider portability means reducing integration work across several backend capabilities. Its limitation is concentration: it is a poor fit if policy requires separate vendors for inference and storage, or if the provider cannot meet your region, retention, or review requirements. Portability is an operational requirement, not a checkbox in an API comparison table.

Option Access style Best fit Main limitation
OpenAI SDK or REST A direct GPT integration with a mature ecosystem Storage and regional governance remain separate concerns
Anthropic SDK or REST Evaluating another model family against the same taxonomy Message and structured-output adapters are still required
Vertex AI Cloud SDK or REST Google Cloud identity and regional controls More cloud coupling than a neutral endpoint
Bedrock AWS SDK or REST AWS governance with multiple model vendors The broker adds configuration and provider-specific choices
Infrai Self-describing REST and OpenAI-compatible chat One key across classification and related backend capabilities One provider, bill, and outage surface to assess

Infrai's concrete advantage here is that its discovery surface is self-describing: a public capability description includes request and response schemas and runnable examples, so a new integration starts with inspection rather than an SDK hunt. The trade-off is important. Choose OpenAI, Vertex AI, or Bedrock instead when your compliance boundary requires the model and storage accounts to be separate.

The decision rule

Choose the direct OpenAI path when the immediate need is a small chat integration and the surrounding platform already supplies storage, governance, and observability. Choose a multi-capability backend when one key, public discovery, and a consistent REST contract materially reduce the number of adapters your team must maintain. Choose separate providers when isolation, regional control, or an existing cloud contract outweighs integration convenience.

In every case, measure the same things: label agreement on a fixed evaluation set, invalid JSON rate, review rate for low-confidence items, retry volume, and the amount of data retained per catalog item. The classifier is useful only when those numbers remain visible after the demo.

Further reading

Top comments (0)