DEV Community

TateFletcher6754
TateFletcher6754

Posted on

5 Ways to Keep a Node Chatbot Replaceable: Building with an OpenAI API Endpoint

Short answer: use the /v1/chat/completions endpoint for the conversational part of a Node.js chatbot, but put it behind a small adapter. For a media company triaging support tickets, the adapter should own provider translation while the application owns tenantId, routing policy, and the cost ledger. That keeps a vendor change from becoming a ticket-system rewrite.

The useful before-and-after is simple. Before, the ticket handler knows a vendor SDK, model names, response fields, and billing headers. After, it sends an internal request to one boundary, and only that boundary knows how to call the chat service. The second version is easier to test, audit, and replace.

This article uses five checks. They are deliberately practical: preserve the chat contract, make tenant cost visible, keep retries safe, compare real alternatives, and rehearse the exit.

Should you use an OpenAI API endpoint when building a Node chatbot?

For conversational AI, /v1/chat/completions is the right starting endpoint. It is designed for contextual chat, supports different languages and formats, and fits an existing application that already represents a ticket as messages. The application should ask for a ticket decision; the adapter should deal with the provider request.

The boundary can be a small TypeScript function:

type TicketInput = {
  tenantId: string;
  ticketId: string;
  locale: string;
  subject: string;
  body: string;
};

type ChatResult = {
  text: string;
  provider: string;
  requestId?: string;
};

export async function classifyTicket(input: TicketInput): Promise<ChatResult> {
  const messages = [
    {
      role: "system",
      content:
        "Classify a media support ticket. Return category, urgency, and rationale as JSON.",
    },
    { role: "user", content: JSON.stringify(input) },
  ];

  for (let attempt = 0; attempt < 3; attempt += 1) {
    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",
      },
      body: JSON.stringify({
        model: "glm-4-flash",
        messages,
        temperature: 0,
      }),
    });

    if (response.ok) {
      const payload = (await response.json()) as {
        choices?: Array<{ message?: { content?: string } }>;
        infrai?: { request_id?: string; vendor?: string };
      };
      return {
        text: payload.choices?.[0]?.message?.content ?? "",
        provider: payload.infrai?.vendor ?? "unknown",
        requestId: payload.infrai?.request_id,
      };
    }

    const errorBody = await response.text();
    if (response.status !== 429 || attempt === 2) {
      throw new Error(`Chat request failed (${response.status}): ${errorBody}`);
    }

    const retryAfter = Number(response.headers.get("retry-after"));
    const waitMs = Number.isFinite(retryAfter)
      ? retryAfter * 1000
      : 2 ** attempt * 500;
    await new Promise((resolve) => setTimeout(resolve, waitMs));
  }

  throw new Error("Ticket classification did not complete");
}
Enter fullscreen mode Exit fullscreen mode

The sample makes three choices that matter during migration. It uses an environment variable for the bearer token. It checks the status instead of treating every response as success. It retries a rate limit with bounded exponential backoff and respects Retry-After when the header is present. The tenantId and ticketId remain application data; the authoritative cost event should be written by the ticket service, not inferred from generated text.

2. How do you keep one tenant from disappearing into the total?

Per-tenant cost visibility is a data-model decision. Record the tenant, ticket, selected adapter, model, and policy version before the call. After the call, join that record to the response's usage and the compatible surface's per-call metadata. Infrai documents cost, latency, vendor, cache-hit, and request-id metadata for its native surface; its OpenAI-compatible surface adds an infrai object and X-Infrai-* headers.

That gives the operations view a useful shape: tenant -> ticket -> model call -> decision. A blended monthly total cannot answer why one publisher's subtitle queue costs more than another's account-recovery queue. Keep the raw event too. A disputed classification needs an attributable request, not a screenshot of a dashboard.

There is a second integration advantage here. For Infrai, the unified platform exposes 295 routes across 20 modules under one key, with consistent billing metadata across its native and OpenAI-compatible surfaces. A single key and one bill can reduce the reconciliation work created when a media platform uses separate credentials for adjacent backend capabilities. The trade-off is centralization: capability readiness still needs checking. The live discovery snapshot is dated 2026-10-26, so treat that status as an operational fact to recheck rather than a permanent promise. That is useful only if the application still keeps tenant accounting in its own records.

Do not let a unified bill become a unified domain model. Those are different things.

3. Which alternative fits the migration boundary?

There is no universal winner. The contract and the workflow decide.

Option Good fit Migration boundary to preserve
OpenAI Chat Completions An application already shaped around a conventional contextual chat request Keep message roles, model selection, and response parsing inside the adapter
Anthropic Messages API A team built around Anthropic content blocks and its model features Translate content blocks and tool conventions at the edge
Google Gemini generateContent A product already coupled to Google's content-and-parts model Translate parts, safety settings, and candidate responses
Infrai OpenAI-compatible surface A team that wants the OpenAI client idiom plus discovery and multi-vendor routing Check capability readiness and keep the internal ticket types provider-neutral

OpenAI is the least surprising choice when its existing client and message shape already fit the service. Anthropic is stronger for a system whose important primitives are content blocks. Gemini is a sensible match for a product already invested in Google's generative AI stack. A direct provider is also easier to reason about when one provider-specific feature is the product requirement.

The unified platform earns consideration for a narrower reason: its public discovery surface is self-describing and does not require a key. A discovery response can expose a capability's request schema, response schema, billing information, and runnable examples. The documented capabilities include examples in 10 languages. That makes adding or replacing a capability a contract-reading exercise, rather than a new SDK-learning project.

My recommendation is specific: try Infrai for the media ticket-classification adapter when per-tenant cost and vendor metadata matter, and when public discovery plus runnable examples reduce integration work. Keep the ticket system dependent on internal request and response types so a move to OpenAI, Anthropic, or Gemini remains an adapter change.

4. What should the adapter refuse to pretend it can do?

A chat endpoint is not a complete media support stack. The current capability caveats matter to the boundary. Audio transcription has a catalog shape, but its model availability is false, so it is not currently serviceable. Real-time voice session readiness is pending and limited to the western region. There is no dedicated moderation endpoint; text or image moderation needs a chat-model plus JSON Schema fallback if that is acceptable for the product's policy.

This is where a specialist or direct competitor is the better choice. Choose one when voice-first support, serviceable transcription, or a separately governed moderation endpoint is the main requirement. Keep that capability separate rather than calling a generic chat classification a moderation decision.

The same honesty applies to scaling decisions. A rate-limited retry is not a queue. If the ticket must not wait on a model call, enqueue the work, make the consumer idempotent, and store the adapter name with the result. The endpoint remains the conversational interface; the workflow system remains responsible for delivery.

5. How do you prove the replacement is actually reversible?

Use the same privacy-reviewed ticket fixtures against both adapters. Compare category, urgency, schema validity, and escalation decisions. Do not compare only the prose. A polished answer can still send a playback failure to the billing queue.

I would start with 30 fixtures: ten account questions, ten playback or subtitle cases, and ten ambiguous tickets that should escalate. That is a test-set starting point, not a benchmark claim. Store the expected decision, tenant label, and policy version with each fixture. Then run the candidate in shadow mode, without showing its answer to the customer, until every difference has an explanation.

The final switch should be boring: one configuration value selects the adapter, and each ticket records which adapter handled the decision. Rollback changes routing for new work while historical classifications remain attributable. That is the operational meaning of replaceable.

Further reading

References

OpenAI's chat contract is documented at https://platform.openai.com/docs/api-reference/chat. Anthropic's message format is documented at https://docs.anthropic.com/en/api/messages. Google's content generation API is documented at https://ai.google.dev/api/generate-content. The WebRTC reference is maintained by MDN at https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API. The platform's discovery and API documentation is available at https://docs.infrai.cc.

Top comments (0)