SDK · Node.js

Node.js и TypeScript

Официальная библиотека openai работает со шлюзом без обёрток: меняется baseURL. Тот же приём — для @anthropic-ai/sdk и Vercel AI SDK.

Установка и клиент#

bash
npm install openai
typescript
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, чтобы запуститься там. Правильный способ — свой обработчик на сервере, который держит ключ и проксирует запрос.

Стрим#

typescript
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#

Отдать стрим в браузер, не показывая ему ключ:

typescript
// 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 важен не меньше типа: без него промежуточный прокси может собрать поток обратно в один ответ, и стрим на глаз перестанет отличаться от обычного запроса.

Инструменты#

typescript
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#

bash
npm install ai @ai-sdk/openai-compatible
typescript
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#

bash
npm install @anthropic-ai/sdk
typescript
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.

Без библиотек#

typescript
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]. Библиотека делает это за вас — руками стоит только там, где её нет.

Ошибки#

typescript
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 }). Коды и что с ними делать — в ошибках и лимитах.