Node.js и TypeScript
Официальная библиотека openai работает со шлюзом без обёрток: меняется baseURL. Тот же приём — для @anthropic-ai/sdk и Vercel AI SDK.
Установка и клиент#
npm install openai
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://nirastudio.org/v1",
apiKey: process.env.NIRA_API_KEY,
});
const resp = await client.chat.completions.create({
model: "anthropic/claude-sonnet-4.6",
messages: [{ role: "user", content: "Привет" }],
});
console.log(resp.choices[0].message.content);Только на сервере
В браузере ключ виден любому, кто откроет вкладку разработчика. Библиотека даже требует явногоdangerouslyAllowBrowser, чтобы запуститься там. Правильный способ — свой обработчик на сервере, который держит ключ и проксирует запрос.Стрим#
const stream = await client.chat.completions.create({
model: "anthropic/claude-sonnet-4.6",
messages: [{ role: "user", content: "Расскажи про событийный цикл" }],
stream: true,
stream_options: { include_usage: true },
});
let usage;
for await (const chunk of stream) {
if (chunk.usage) usage = chunk.usage;
const piece = chunk.choices[0]?.delta?.content;
if (piece) process.stdout.write(piece);
}
console.log("\nтокенов:", usage?.total_tokens ?? "—");Необязательная цепочка ?. здесь не для красоты: у куска с usage массив choices пустой — см. стриминг.
Обработчик Next.js#
Отдать стрим в браузер, не показывая ему ключ:
// app/api/ask/route.ts
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://nirastudio.org/v1",
apiKey: process.env.NIRA_API_KEY,
});
export async function POST(request: Request) {
const { prompt } = await request.json();
const upstream = await client.chat.completions.create({
model: "anthropic/claude-sonnet-4.6",
messages: [{ role: "user", content: prompt }],
stream: true,
});
const encoder = new TextEncoder();
const body = new ReadableStream({
async start(controller) {
for await (const chunk of upstream) {
const piece = chunk.choices[0]?.delta?.content;
if (piece) controller.enqueue(encoder.encode(piece));
}
controller.close();
},
});
return new Response(body, {
headers: {
"Content-Type": "text/plain; charset=utf-8",
"Cache-Control": "no-cache, no-transform",
},
});
}no-transform важен не меньше типа: без него промежуточный прокси может собрать поток обратно в один ответ, и стрим на глаз перестанет отличаться от обычного запроса.
Инструменты#
import type OpenAI from "openai";
const tools: OpenAI.ChatCompletionTool[] = [{
type: "function",
function: {
name: "get_weather",
description: "Текущая погода в городе",
parameters: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
},
},
}];
const messages: OpenAI.ChatCompletionMessageParam[] = [
{ role: "user", content: "Погода в Москве?" },
];
for (;;) {
const resp = await client.chat.completions.create({
model: "anthropic/claude-sonnet-4.6",
messages,
tools,
});
const msg = resp.choices[0].message;
messages.push(msg); // вызов должен остаться в истории
if (!msg.tool_calls?.length) {
console.log(msg.content);
break;
}
for (const call of msg.tool_calls) {
const { city } = JSON.parse(call.function.arguments);
messages.push({
role: "tool",
tool_call_id: call.id,
content: `${city}: -3 °C, снег`,
});
}
}Разбор формата и tool_choice — в инструментах.
Vercel AI SDK#
npm install ai @ai-sdk/openai-compatible
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
import { streamText } from "ai";
const nira = createOpenAICompatible({
name: "nira",
baseURL: "https://nirastudio.org/v1",
apiKey: process.env.NIRA_API_KEY!,
});
const result = streamText({
model: nira("anthropic/claude-sonnet-4.6"),
prompt: "Объясни разницу между кэшем и памятью",
});
for await (const piece of result.textStream) process.stdout.write(piece);Берите именно openai-compatible, а не @ai-sdk/openai: второй знает свой список моделей и на незнакомом имени спорит.
Библиотека Anthropic#
npm install @anthropic-ai/sdk
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({
baseURL: "https://nirastudio.org",
apiKey: process.env.NIRA_API_KEY,
});
const msg = await client.messages.create({
model: "anthropic/claude-sonnet-4.6",
max_tokens: 1024,
messages: [{ role: "user", content: "Привет" }],
});
console.log(msg.content[0].type === "text" ? msg.content[0].text : "");Адрес — без /v1, путь библиотека дописывает сама. События этого протокола — в Anthropic Messages.
Без библиотек#
const res = await fetch("https://nirastudio.org/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NIRA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "anthropic/claude-sonnet-4.6",
messages: [{ role: "user", content: "Привет" }],
}),
});
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
const data = await res.json();
console.log(data.choices[0].message.content);fetchне бросает исключение на4xx— проверкаres.okобязательна, иначе ошибка тихо превратится вundefinedдальше по коду.- Для стрима придётся разбирать SSE самому: читать
res.body, делить по пустой строке, отбрасывать префиксdata:и останавливаться на[DONE]. Библиотека делает это за вас — руками стоит только там, где её нет.
Ошибки#
import OpenAI from "openai";
try {
await client.chat.completions.create({ /* … */ });
} catch (e) {
if (e instanceof OpenAI.APIError) {
if (e.status === 402) console.error("баланс кончился");
else if (e.status === 429) console.error("дневной лимит ключа");
else console.error(e.status, e.message);
} else {
throw e;
}
}Повторы 429 и 502 библиотека берёт на себя: new OpenAI({ maxRetries: 3 }). Коды и что с ними делать — в ошибках и лимитах.