1. Установка и ключ
Установите официальный пакет openai — отдельного клиента NormaHub не существует, совместимость достигается сменой baseURL. Ключ храните в переменных окружения процесса и никогда не коммитьте .env в репозиторий.
npm install openai
export NORMAHUB_API_KEY="ваш_api_ключ"Создайте API-ключ в личном кабинете NormaHub и скопируйте значение сразу: повторно оно не показывается. Под разные окружения (dev, prod, конкретный бот) заводите отдельные ключи с собственными лимитами расходов.
2. Первый запрос
Клиент работает как с оригинальным OpenAI: та же сигнатура create, те же choices. Отличие только в двух строках конфигурации — apiKey из окружения и baseURL NormaHub. Длину ответа ограничивайте max_tokens: это и защита от зависших диалогов, и контроль бюджета.
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: "gpt-5.6-sol",
messages: [{ role: "user", content: "Привет" }],
max_tokens: 512,
});
console.log(response.choices[0].message.content);Модель указывайте точным ID из актуального каталога. После первого успешного ответа сверьте usage и стоимость в истории личного кабинета — успешный HTTP-ответ ещё не означает корректный биллинг.
3. Streaming ответов
Потоковый режим отдаёт токены по мере генерации: интерфейс оживает мгновенно, а пользователь не смотрит на спиннер. Поток гарантирован только через Responses API — для /v1/chat/completions и /v1/messages отправляйте stream: false. Обрабатывайте обрыв отдельно от нормального завершения — часть ответа могла уже уйти пользователю, а сервер — завершить операцию.
const stream = await client.responses.stream({
model: "gpt-5.6-sol",
input: "Объясни коротко",
});
for await (const event of stream) {
console.log(event);
}Streaming — свойство конкретного endpoint и модели. Если поток не поддержан, используйте обычный режим, а не бесконечные повторы. Детали событий и завершения — в гайде по streaming.
4. Обработка ошибок
SDK бросает типизированные исключения: AuthenticationError для 401, RateLimitError для 429, APIError с полем status для остальных. Разделяйте постоянные ошибки (400, 401, 402, 403 — чинить причину) и временные (429, 5xx — повторять с backoff). Всегда задавайте timeout: зависший запрос хуже быстрой ошибки.
import OpenAI from "openai";
try {
await client.chat.completions.create({
model: "gpt-5.6-sol",
messages: [{ role: "user", content: "Привет" }],
timeout: 30_000,
});
} catch (error) {
if (error instanceof OpenAI.AuthenticationError) {
console.error("401: ключ неверен или отозван");
} else if (error instanceof OpenAI.RateLimitError) {
console.error("429: лимит ключа, повторите позже");
} else if (error instanceof OpenAI.APIError) {
console.error(`HTTP ${error.status}: смотрите гайд по ошибкам`);
} else {
throw error;
}
}Сохраняйте X-Request-Id каждого запроса — по нему поддержка находит операцию без вашего ключа и промпта. Полная таблица кодов и retry-политик — в гайде по ошибкам API.
5. Безопасность ключа
Ключ должен оставаться на сервере. Для браузерного приложения создайте собственный защищённый backend route: фронт отправляет сообщение вашему серверу, сервер добавляет секретный ключ и обращается к NormaHub. По пути обрежьте длину входного сообщения и ограничьте max_tokens — иначе чужой скрипт через вашу форму потратит весь баланс.
// app/api/chat/route.ts — ключ живёт только на сервере
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: "gpt-5.6-sol",
messages: [{ role: "user", content: String(message).slice(0, 4000) }],
max_tokens: 512,
});
return Response.json({ text: response.choices[0].message.content });
}Дополнительно включайте rate limit на своём route и логируйте только статусы и request ID, а не ключи и содержимое. Диагностику упавших вызовов начинайте с кабинета: баланс, лимит ключа, затем таблица кодов.