NormaHub knowledge base

First request with Node.js

OpenAI SDK with the NormaHub baseURL: setup, sync and streaming requests, error handling and a secure backend route.

1. Setup and key

Install the official openai package — there is no separate NormaHub client, compatibility comes from switching the baseURL. Keep the key in the process environment variables and never commit .env to the repository. Russian version of this guide: AI API на Node.js.

npm install openai
export NORMAHUB_API_KEY="your_api_key"

Create an API key in the NormaHub dashboard and copy the value once: it is not shown again. Issue separate keys with their own spend limits for different environments (dev, prod, a specific bot).

2. First request

The client works like the original OpenAI: the same create signature, the same choices. Only two lines of configuration differ — apiKey from the environment and the NormaHub baseURL. Cap the answer length with max_tokens: it guards against runaway dialogs and controls the budget.

import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.NORMAHUB_API_KEY,
  baseURL: "https://api.normahub.cc/v1",
});

const response = await client.chat.completions.create({
  model: "MODEL_ID",
  messages: [{ role: "user", content: "Hello" }],
  max_tokens: 512,
});

console.log(response.choices[0].message.content);

Pass the model as the exact ID from the current catalog. After the first successful answer, cross-check usage and cost in the dashboard history — a successful HTTP response does not yet mean correct billing.

3. Streaming responses

Streaming mode delivers tokens as they generate: the interface comes alive instantly and the user never stares at a spinner. Handle an interruption separately from a normal finish — part of the answer may already have reached the user while the server considers the operation done.

const stream = await client.chat.completions.create({
  model: "MODEL_ID",
  messages: [{ role: "user", content: "Explain briefly" }],
  stream: true,
});

for await (const chunk of stream) {
  process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}

Streaming is a property of the specific endpoint and model. If streaming is not supported, use the regular mode instead of endless retries. Event and completion details are in the streaming guide.

4. Error handling

The SDK throws typed exceptions: AuthenticationError for 401, RateLimitError for 429, APIError with a status field for the rest. Split permanent errors (400, 401, 402, 403 — fix the cause) from temporary ones (429, 5xx — retry with backoff). Always set a timeout: a hanging request is worse than a fast error.

import OpenAI from "openai";

try {
  await client.chat.completions.create({
    model: "MODEL_ID",
    messages: [{ role: "user", content: "Hello" }],
    timeout: 30_000,
  });
} catch (error) {
  if (error instanceof OpenAI.AuthenticationError) {
    console.error("401: key invalid or revoked");
  } else if (error instanceof OpenAI.RateLimitError) {
    console.error("429: key limit hit, retry later");
  } else if (error instanceof OpenAI.APIError) {
    console.error(`HTTP ${error.status}: see the errors guide`);
  } else {
    throw error;
  }
}

Save the X-Request-Id of every request — support finds the operation by it without your key or prompt. The full code table and retry policy are in the API errors guide.

5. Key security

The key must stay on the server. For a browser application, create your own protected backend route: the frontend sends the message to your server, the server attaches the secret key and calls NormaHub. Along the way, trim the input message length and cap max_tokens — otherwise someone else's script through your form will spend the whole balance.

// app/api/chat/route.ts — the key lives on the server only
import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.NORMAHUB_API_KEY,
  baseURL: "https://api.normahub.cc/v1",
});

export async function POST(request: Request) {
  const { message } = await request.json();
  const response = await client.chat.completions.create({
    model: "MODEL_ID",
    messages: [{ role: "user", content: String(message).slice(0, 4000) }],
    max_tokens: 512,
  });
  return Response.json({ text: response.choices[0].message.content });
}

Additionally, rate-limit your own route and log only statuses and request IDs, never keys or content. When errors appear, check the balance and available models in the dashboard.

Частые вопросы

Where do I get the model ID for a Node.js request?

Use the exact ID from the models catalog or from GET /v1/models with your key. Capabilities (tools, streaming) depend on the specific model — check them before switching.

Can I call the API directly from the browser?

No. A secret key must never be embedded in client-side JavaScript: it gets extracted from the bundle in a minute. Build your own backend route (example above) and let the browser talk to it.

How do I handle 429 and 503?

These are temporary errors: retry with exponential delay, honor Retry-After, and cap the total attempts and the operation deadline. Persistent 401/402/403 are never fixed by retrying.

Do I need a VPN for requests from Russia?

No. The client talks to https://api.normahub.cc/v1 directly. Billing comes from the ruble balance topped up in a convenient way.