Как делать12 октября 2026 г.6 мин чтения
AI-интеграцииШаг 2 из 8

LLM API: первый запрос к Claude и OpenAI

Коротко

Чтобы вызвать языковую модель из кода, установите официальный SDK (@anthropic-ai/sdk или openai), положите ключ в переменную окружения на сервере и отправьте запрос: у Anthropic это client.messages.create с полями model, max_tokens, system и messages, у OpenAI — client.responses.create с model, instructions и input. Для интерфейсов включают стриминг, а ошибки 429 и 5xx SDK по умолчанию повторяет сам; ваша задача — задать лимиты, таймаут и разобрать оставшиеся ошибки по типам.

Пошаговый урокУровень: НачальныйAPI языковых моделейПрактика: ~1,5–3 ч

После материала вы сможете

  • отправить запрос к Anthropic Messages API и OpenAI Responses API через официальные SDK, взяв ключ из переменной окружения
  • передать системную инструкцию и историю диалога и извлечь текст ответа без привязки к позиции блока
  • включить стриминг у обоих провайдеров и обработать ошибку, пришедшую посреди потока
  • настроить maxRetries и timeout, отличить временную ошибку 429 от исчерпанной квоты и посчитать расход по usage

Проверено на

Node.js 24 LTS (24.18)TypeScript 7.0 (tsc 7.0.2)@anthropic-ai/sdk 0.133.0openai (npm) 7.32.0@types/node 24claude-opus-5-5, gpt-6-astra (2026-10-12)

Сверено с документацией: 12 октября 2026 г.

Содержание
  1. 01Что понадобится и где хранить ключ
  2. 02Первый запрос к Claude: Messages API
  3. 03Первый запрос к OpenAI: Responses API
  4. 04Роли сообщений и системная инструкция
  5. 05Стриминг ответа
  6. 06Ошибки, лимиты и повторы
  7. 07Общий интерфейс, учёт токенов и проверка

Что понадобится и где хранить ключ

Урок рассчитан на разработчика, который уже пишет на TypeScript и работает с async/await. Понадобятся Node.js 24, аккаунты в консоли Anthropic и в платформе OpenAI и по одному API-ключу. Ключ — это пароль от вашего счёта: любой, кто его получил, тратит ваши деньги. Поэтому правило одно: ключ живёт только на сервере, в переменной окружения, и никогда не попадает в браузер, мобильное приложение или репозиторий.

bash
mkdir llm-first-request
cd llm-first-request
npm init -y
npm pkg set type=module
npm install @anthropic-ai/sdk openai
npm install -D typescript @types/node@24
mkdir src
Проект: ES-модули, TypeScript только для проверки типов — Node.js 24 запускает .ts напрямую
tsconfig.json
{
  "compilerOptions": {
    "target": "es2024",
    "module": "nodenext",
    "strict": true,
    "noEmit": true,
    "allowImportingTsExtensions": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true,
    "skipLibCheck": true,
    "types": ["node"]
  },
  "include": ["src"]
}
Строгая проверка типов без сборки: tsc сверяет код с типами обоих SDK
.env.example
# Copy to .env and put your own keys there. Never commit .env.
ANTHROPIC_API_KEY=YOUR_ANTHROPIC_API_KEY
OPENAI_API_KEY=YOUR_OPENAI_API_KEY
Шаблон коммитится, а копия .env с настоящими ключами — нет: добавьте .env в .gitignore

Первый запрос к Claude: Messages API

У Anthropic всё идёт через один эндпоинт — Messages API. Минимальный запрос содержит идентификатор модели, лимит длины ответа max_tokens и массив messages; системная инструкция передаётся отдельным полем system. Идентификатор модели берите со страницы моделей: на 12.10.2026 документация советует начинать с claude-opus-5-5, а для массовых простых задач есть более быстрая и дешёвая claude-haiku-5-5.

src/anthropic-hello.ts
import Anthropic from "@anthropic-ai/sdk";

// The SDK reads ANTHROPIC_API_KEY from the environment; fail fast with a clear message.
if (!process.env.ANTHROPIC_API_KEY) {
  throw new Error("ANTHROPIC_API_KEY is not set: copy .env.example to .env");
}

const client = new Anthropic();

const message = await client.messages.create({
  model: "claude-opus-5-5",
  max_tokens: 2048,
  system: "You are a concise assistant for a web studio. Answer in two sentences.",
  messages: [{ role: "user", content: "What is an HTTP 429 status code?" }],
});

// content is an array of blocks (text, thinking, tool_use...): take only text blocks.
const text = message.content
  .filter((block) => block.type === "text")
  .map((block) => block.text)
  .join("");

console.log(text);
console.log("stop_reason:", message.stop_reason);
console.log("tokens in/out:", message.usage.input_tokens, message.usage.output_tokens);
Первый запрос: системная инструкция, извлечение текста по типу блока, учёт токенов
bash
npx tsc -p .
node --env-file=.env src/anthropic-hello.ts
node --env-file=.env src/openai-hello.ts
Проверка типов и запуск: флаг --env-file загружает переменные из .env без сторонних пакетов
  • content — массив блоков, а не строка. У текущих моделей Claude рассуждение включено всегда, поэтому перед текстом может прийти блок thinking; брать content[0].text нельзя — фильтруйте по type === "text"
  • max_tokens — жёсткий потолок ответа, и рассуждения модели тоже в него входят. Если пришёл stop_reason: "max_tokens", ответ обрезан
  • stop_reason: "end_turn" — модель закончила сама; "refusal" — отказалась выполнять запрос, такой ответ нельзя показывать как обычный
  • usage.input_tokens и usage.output_tokens — фактический расход, по нему считают стоимость

Первый запрос к OpenAI: Responses API

У OpenAI два текстовых API: старый Chat Completions и Responses. Документация OpenAI рекомендует для новых приложений Responses API, а с моделями-рассуждателями — тем более. Запрос проще: input может быть строкой, системная инструкция — полем instructions, а готовый текст SDK собирает в response.output_text. На 12.10.2026 в документации OpenAI рекомендованы gpt-6-astra для сложных задач, gpt-6.1-sol как баланс цены и качества и gpt-6-luna для массовых задач.

src/openai-hello.ts
import OpenAI from "openai";

// The SDK reads OPENAI_API_KEY from the environment; fail fast with a clear message.
if (!process.env.OPENAI_API_KEY) {
  throw new Error("OPENAI_API_KEY is not set: copy .env.example to .env");
}

const client = new OpenAI();

const response = await client.responses.create({
  model: "gpt-6-astra",
  reasoning: { effort: "low" },
  instructions: "You are a concise assistant for a web studio. Answer in two sentences.",
  input: "What is an HTTP 429 status code?",
});

// output_text joins every text item from response.output into one string.
console.log(response.output_text);
console.log("status:", response.status);
console.log("tokens in/out:", response.usage?.input_tokens, response.usage?.output_tokens);
Тот же вопрос через Responses API; усилие рассуждения снижено для простого ответа

Роли сообщений и системная инструкция

LLM API не хранит диалог: каждый запрос несёт всю историю, которую модель должна учесть. История — это список сообщений с ролями user (пользователь) и assistant (прежние ответы модели). Системная инструкция задаёт роль, правила и формат для всего разговора и важнее отдельных сообщений. У Anthropic это поле system, у OpenAI — instructions (или сообщение с ролью developer в input).

src/history.ts
import Anthropic from "@anthropic-ai/sdk";
import OpenAI from "openai";

type Turn = { role: "user" | "assistant"; content: string };

const system = "You are a support assistant of a coffee shop. Answer briefly.";

// The API is stateless: every request carries the whole conversation.
const history: Turn[] = [
  { role: "user", content: "Do you have oat milk?" },
  { role: "assistant", content: "Yes, oat and almond milk are available." },
  { role: "user", content: "Is it extra?" },
];

const anthropic = new Anthropic();
const claude = await anthropic.messages.create({
  model: "claude-opus-5-5",
  max_tokens: 2048,
  system, // Anthropic: the system prompt is a separate top-level field
  messages: history,
});
const claudeText = claude.content
  .filter((block) => block.type === "text")
  .map((block) => block.text)
  .join("");
console.log("Claude:", claudeText);

const openai = new OpenAI();
const gpt = await openai.responses.create({
  model: "gpt-6-astra",
  instructions: system, // OpenAI: instructions outrank messages in input
  input: history,
});
console.log("GPT:", gpt.output_text);
Один и тот же диалог для обоих провайдеров: история целиком в каждом запросе

Системная инструкция

Anthropic Messages API
system
OpenAI Responses API
instructions или роль developer

История диалога

Anthropic Messages API
messages: user / assistant
OpenAI Responses API
input: строка или массив сообщений

Лимит ответа

Anthropic Messages API
max_tokens (обязателен)
OpenAI Responses API
max_output_tokens (необязателен)

Текст ответа

Anthropic Messages API
блоки content с type: "text"
OpenAI Responses API
output_text

Почему остановилась

Anthropic Messages API
stop_reason
OpenAI Responses API
status и incomplete_details

Расход

Anthropic Messages API
usage.input_tokens / output_tokens
OpenAI Responses API
usage.input_tokens / output_tokens

Стриминг ответа

Без стриминга пользователь ждёт, пока модель сгенерирует весь ответ. Со стримингом (server-sent events, SSE — поток событий по одному HTTP-ответу) текст появляется по мере генерации. В Anthropic SDK удобнее всего помощник messages.stream(): событие text отдаёт куски текста, а finalMessage() возвращает собранное сообщение с stop_reason и usage. В OpenAI SDK запрос с stream: true возвращает асинхронный поток типизированных событий: текст приходит в response.output_text.delta, конец — в response.completed.

src/stream.ts
import Anthropic from "@anthropic-ai/sdk";
import OpenAI from "openai";

const question = "List three checks before deploying a website.";

// Anthropic: the stream helper emits text chunks and assembles the final message.
const anthropic = new Anthropic();
const stream = anthropic.messages
  .stream({
    model: "claude-opus-5-5",
    max_tokens: 2048,
    messages: [{ role: "user", content: question }],
  })
  .on("text", (chunk) => process.stdout.write(chunk));

const final = await stream.finalMessage();
console.log("\n[claude] stop_reason:", final.stop_reason, "output tokens:", final.usage.output_tokens);

// OpenAI: stream: true returns an async iterable of typed events.
const openai = new OpenAI();
const events = await openai.responses.create({
  model: "gpt-6-astra",
  input: question,
  stream: true,
});

for await (const event of events) {
  if (event.type === "response.output_text.delta") {
    process.stdout.write(event.delta);
  } else if (event.type === "response.completed") {
    console.log("\n[gpt] status:", event.response.status, "output tokens:", event.response.usage?.output_tokens);
  } else if (event.type === "error") {
    console.error("\n[gpt] stream error:", event.message);
  }
}
Стриминг у обоих провайдеров: печатаем куски по мере прихода и итоговый расход

Ошибки, лимиты и повторы

Оба SDK бросают типизированные ошибки — наследников APIError с полем status: AuthenticationError (401), RateLimitError (429), InternalServerError (5xx), APIConnectionError и APIConnectionTimeoutError для сети и таймаута. Временные сбои — сетевые ошибки, 408, 409, 429 и 5xx — SDK по умолчанию повторяет сам два раза с экспоненциальной задержкой, учитывая заголовок retry-after. Число повторов задаёт maxRetries, таймаут — timeout в миллисекундах (по умолчанию 10 минут).

400

Что случилось
Неверный запрос: параметр, формат, неподдерживаемое значение
Что делать
Не повторять, исправить запрос; залогировать requestID

401 / 403

Что случилось
Неверный, отозванный ключ или нет прав
Что делать
Не повторять, проверить ключ и доступ

429 с retry-after

Что случилось
Превышен лимит запросов или токенов в минуту
Что делать
Подождать указанное время, снизить темп или поставить в очередь

429 без retry-after или с кодом квоты

Что случилось
Исчерпан бюджет, кредит или лимит расходов
Что делать
Повтор не поможет: пополнить баланс или поднять лимит

500, 503, 529

Что случилось
Сбой или перегрузка на стороне провайдера
Что делать
Повтор с паузой; SDK делает это сам

Сеть или таймаут

Что случилось
Нет ответа за отведённое время
Что делать
Повтор; для длинных ответов — стриминг
src/llm.ts
import Anthropic from "@anthropic-ai/sdk";
import OpenAI from "openai";

export type Provider = "anthropic" | "openai";

export type AskInput = {
  system: string;
  prompt: string;
  maxOutputTokens?: number;
};

export type AskResult = {
  provider: Provider;
  model: string;
  text: string;
  inputTokens: number;
  outputTokens: number;
  complete: boolean; // false when the answer was cut off by the token limit
};

export class LLMError extends Error {
  readonly provider: Provider;
  readonly status: number | undefined;
  readonly retryable: boolean; // temporary problem: try later or put the job in a queue

  constructor(provider: Provider, message: string, options: { status?: number; retryable: boolean; cause?: unknown }) {
    super(message, { cause: options.cause });
    this.name = "LLMError";
    this.provider = provider;
    this.status = options.status;
    this.retryable = options.retryable;
  }
}

// Model IDs: copy them from the providers' model pages, do not guess.
const MODELS = { anthropic: "claude-opus-5-5", openai: "gpt-6-astra" } as const;

// SDK-level retries with exponential backoff (429, 408, 409, 5xx, network) and a request timeout.
const CLIENT_OPTIONS = { maxRetries: 3, timeout: 60_000 };

function requireKey(name: "ANTHROPIC_API_KEY" | "OPENAI_API_KEY"): string {
  const value = process.env[name];
  if (!value) throw new Error(`${name} is not set: copy .env.example to .env`);
  return value;
}

// Clients are created on first use, so a missing key of one provider does not break the other.
let anthropic: Anthropic | undefined;
let openai: OpenAI | undefined;

async function askAnthropic(input: AskInput): Promise<AskResult> {
  anthropic ??= new Anthropic({ apiKey: requireKey("ANTHROPIC_API_KEY"), ...CLIENT_OPTIONS });
  const message = await anthropic.messages.create({
    model: MODELS.anthropic,
    max_tokens: input.maxOutputTokens ?? 2048,
    system: input.system,
    messages: [{ role: "user", content: input.prompt }],
  });
  if (message.stop_reason === "refusal") {
    throw new LLMError("anthropic", "anthropic: the model declined the request", { retryable: false });
  }
  return {
    provider: "anthropic",
    model: message.model,
    text: message.content
      .filter((block) => block.type === "text")
      .map((block) => block.text)
      .join(""),
    inputTokens: message.usage.input_tokens,
    outputTokens: message.usage.output_tokens,
    complete: message.stop_reason !== "max_tokens",
  };
}

async function askOpenAI(input: AskInput): Promise<AskResult> {
  openai ??= new OpenAI({ apiKey: requireKey("OPENAI_API_KEY"), ...CLIENT_OPTIONS });
  const response = await openai.responses.create({
    model: MODELS.openai,
    reasoning: { effort: "low" },
    instructions: input.system,
    input: input.prompt,
    max_output_tokens: input.maxOutputTokens ?? 2048,
  });
  return {
    provider: "openai",
    model: response.model,
    text: response.output_text,
    inputTokens: response.usage?.input_tokens ?? 0,
    outputTokens: response.usage?.output_tokens ?? 0,
    complete: response.status === "completed",
  };
}

const QUOTA_CODES = new Set([
  "credit_balance_exhausted",
  "organization_spend_limit_exceeded",
  "project_spend_limit_exceeded",
  "organization_usage_limit_exceeded",
]);

// Both SDKs throw the same family of typed errors: check specific classes first.
function toLLMError(provider: Provider, err: unknown): unknown {
  if (err instanceof Anthropic.AuthenticationError || err instanceof OpenAI.AuthenticationError) {
    return new LLMError(provider, `${provider}: invalid or revoked API key`, { status: 401, retryable: false, cause: err });
  }
  if (err instanceof Anthropic.RateLimitError || err instanceof OpenAI.RateLimitError) {
    const wait = err.headers.get("retry-after");
    // A spend or quota limit is not temporary: Anthropic sends that 429 without retry-after,
    // OpenAI marks it with a code such as credit_balance_exhausted.
    const quota = err instanceof OpenAI.RateLimitError ? QUOTA_CODES.has(err.code ?? "") : wait === null;
    const message = quota ? `${provider}: spend or quota limit reached` : `${provider}: rate limit, retry after ${wait ?? "?"} s`;
    return new LLMError(provider, message, { status: 429, retryable: !quota, cause: err });
  }
  if (err instanceof Anthropic.APIConnectionError || err instanceof OpenAI.APIConnectionError) {
    return new LLMError(provider, `${provider}: network error or timeout`, { retryable: true, cause: err });
  }
  if (err instanceof Anthropic.APIError || err instanceof OpenAI.APIError) {
    const status = err.status;
    return new LLMError(provider, `${provider}: HTTP ${status}, request ${err.requestID ?? "-"}`, {
      status,
      retryable: status !== undefined && status >= 500,
      cause: err,
    });
  }
  return err;
}

export async function ask(provider: Provider, input: AskInput): Promise<AskResult> {
  try {
    return provider === "anthropic" ? await askAnthropic(input) : await askOpenAI(input);
  } catch (err) {
    throw toLLMError(provider, err);
  }
}
Практика урока: общий интерфейс к обоим провайдерам с лимитами, повторами и разбором ошибок
  • Повторы с паузами уже встроены в SDK: свой цикл повторов поверх них умножит число запросов, как и повторы на нескольких уровнях системы
  • retryable в LLMError подсказывает вызывающему коду, можно ли поставить задачу в очередь на потом или нужно сразу сообщить об ошибке
  • Лимиты у Anthropic считаются в запросах и входных и выходных токенах в минуту, у OpenAI — в запросах и токенах в минуту и в сутки; текущие значения видны в консоли и в заголовках ответа (anthropic-ratelimit-*, x-ratelimit-*)
  • Для разбора с поддержкой логируйте идентификатор запроса: requestID у ошибки, _request_id у ответа Anthropic

Общий интерфейс, учёт токенов и проверка

Скрипт ниже вызывает обоих провайдеров через одну функцию ask и печатает ответ и расход. Стоимость считают по usage и ценам со страниц цен; цены меняются, поэтому их держат в конфигурации, а не в коде. Учтите, что рассуждения моделей оплачиваются как выходные токены, а история диалога при каждом запросе — снова как входные.

src/compare.ts
import { ask, LLMError, type Provider } from "./llm.ts";

const providers: Provider[] = ["anthropic", "openai"];
const input = {
  system: "You are a concise assistant for a web studio. Answer in two sentences.",
  prompt: "Why should an API key never be stored in frontend code?",
};

for (const provider of providers) {
  try {
    const result = await ask(provider, input);
    console.log(`[${result.provider}] ${result.model}: ${result.text}`);
    console.log(`  tokens in/out: ${result.inputTokens}/${result.outputTokens}${result.complete ? "" : " (cut off)"}`);
  } catch (err) {
    if (!(err instanceof LLMError)) throw err;
    console.error(`[${provider}] ${err.message} (retryable: ${err.retryable})`);
    process.exitCode = 1;
  }
}
Один вопрос — два провайдера; ошибка одного не мешает второму
src/cost.ts
// Prices change: take them from the provider's pricing page and keep them in config, not in code.
export type Price = {
  inputPerMTok: number; // USD per 1M input tokens
  outputPerMTok: number; // USD per 1M output tokens (reasoning tokens are billed as output too)
};

export type Usage = { inputTokens: number; outputTokens: number };

export function costUSD(usage: Usage, price: Price): number {
  return (usage.inputTokens * price.inputPerMTok + usage.outputTokens * price.outputPerMTok) / 1_000_000;
}

// Running total for a batch of requests, e.g. one night of ticket triage.
export function totalUsage(items: Usage[]): Usage {
  return items.reduce(
    (sum, u) => ({ inputTokens: sum.inputTokens + u.inputTokens, outputTokens: sum.outputTokens + u.outputTokens }),
    { inputTokens: 0, outputTokens: 0 },
  );
}
Стоимость запроса и пакета запросов по фактическому usage

Проверить код можно и без реальных ключей и без трат. Оба SDK читают адрес API из переменных ANTHROPIC_BASE_URL и OPENAI_BASE_URL, поэтому мы направили их на локальный мок-сервер, который отвечает в формате документации: обычным JSON, потоком SSE и ошибками 401, 429, 529. На нём прошли все скрипты урока и 14 тестов node:test: повторы после 429 и 529, отказ без повторов на 401, обрезанные ответы, ошибка посреди потока.

terminal
$ node --env-file=.env.mock src/compare.ts
[anthropic] claude-opus-5-5: MOCK CLAUDE: Why should an API key never be stored in frontend code?
  tokens in/out: 24/13
[openai] gpt-6-astra: MOCK GPT: Why should an API key never be stored in frontend code?
  tokens in/out: 24/13
Вывод против локального мок-сервера: с настоящими ключами текст ответа и числа токенов будут другими
  • npx tsc -p . проходит без ошибок
  • При пустой переменной ключа скрипт падает с понятным сообщением и не отправляет запрос
  • Ответ Claude собирается только из блоков text, ответ OpenAI — из output_text
  • Обрезанный ответ помечается: stop_reason: "max_tokens" или status: "incomplete"
  • 429 с retry-after повторяется, 401 и 400 — нет
Как проверено3 · пройдено3 · частичносверено 12 октября 2026 г.
  • запущено, работает

    Подготовка проекта (bash), tsconfig.json, .env.example

    выполнено во временном проекте scratchpad/kb/verify/llm-api-pervyj-zapros: npm init, type=module, установка @anthropic-ai/sdk 0.133.0, openai 7.32.0, typescript 7.0.2, @types/node 24.19.2; tsc -p tsconfig.article.json (skipLibCheck true) и tsc -p tsconfig.json (skipLibCheck false, src+mock+test) — 0 ошибок

  • запущено, работает

    src/cost.ts

    node:test: costUSD и totalUsage на фиксированных числах (тестовые цены, не прайс)

  • запущено, работает

    Вывод compare.ts (shellsession)

    реальный вывод запуска node --env-file=.env.mock src/compare.ts против мок-сервера; в подписи указано, что с настоящими ключами текст и числа будут другими

  • частично

    src/anthropic-hello.ts, src/openai-hello.ts, src/history.ts, src/stream.ts, src/compare.ts

    tsc --noEmit против типов SDK + запуск Node 24.18 (type stripping) против локального мок-сервера (ANTHROPIC_BASE_URL / OPENAI_BASE_URL), который отвечает JSON и SSE в формате документации, в т.ч. блок thinking перед text; node:test проверяет вывод и тело запросов (model, max_tokens, system, instructions, reasoning, stream, история из 3 сообщений), отказ без ключа без отправки запроса. Реальный вызов API — NOT_RUN (ключей нет по условиям задания)

  • частично

    src/llm.ts (общий интерфейс, maxRetries/timeout, разбор ошибок)

    node:test против мока: 429+529 → SDK повторяет и получает ответ (3 запроса); 429×4 → LLMError retryable, 4 запроса (1+maxRetries 3); 429 с кодом credit_balance_exhausted и 429 Anthropic без retry-after → retryable false; 401 → 1 запрос, retryable false; 400 → requestID в сообщении; stop_reason max_tokens и status incomplete → complete false; refusal → LLMError. Поведение реального API на эти ситуации не проверялось

  • частично

    src/ask-stream.ts (решение упражнения)

    node:test против мока: куски складываются в полный ответ у обоих провайдеров; SSE-событие error (overloaded_error) после 200 отклоняет finalMessage()

«Запущено» — код выполнен во временном проекте или контейнере, вывод в статье взят из прогона. «Частично» — проверено с заглушками вместо внешних сервисов. «Не запускалось» — нужны устройство, аккаунт или ключ; шаги сверены с официальной документацией. Файлы проверочного проекта и логи запусков сохранены в архиве InterLAB.

Расскажите о задаче — мы изучим запрос и свяжемся с вами

Есть задача?
Обсудим и оценим — бесплатно.

Расскажите о продукте или проблеме. Предложим архитектуру, срок и стоимость первой версии после изучения задачи.

  • Оценка объёма и рисков до старта
  • NDA — по вашему или нашему шаблону
  • Старт работ в течение недели

Не присылайте через форму пароли, ключи доступа и исходный код — безопасную передачу доступа обсудим отдельно.

Нажимая кнопку, вы соглашаетесь с обработкой персональных данных.