lenec ru

← все посты

YandexGPT API: подключение из Node без боли

15K

На одном из проектов я делал бота для внутренней техподдержки в большой компании, и заказчик попросил российскую LLM "чтобы не зависеть от санкций и блокировок". Выбор стоял между YandexGPT и GigaChat. Взял YandexGPT — он у клиента уже был подключён к Yandex Cloud, ID-каталога есть, осталось только разобраться с API. Расскажу, что я узнал.

Что нужно завести в Yandex Cloud

YandexGPT не существует отдельно — это сервис внутри Yandex Cloud Foundation Models. Минимальный набор шагов, чтобы получить рабочий доступ:

  • Зарегистрироваться в Yandex Cloud, прицепить платёжный аккаунт. Без активного биллинга API не работает.
  • Создать каталог (folder). Запомнить его ID — он понадобится в каждом запросе.
  • Создать сервисный аккаунт, выдать ему роль ai.languageModels.user. Не пытайся ходить на API под обычным IAM-пользователем — для машинного доступа сервисный аккаунт правильнее.
  • Сгенерировать API-ключ для сервисного аккаунта (через консоль или CLI). Это самый простой способ авторизации.
yc iam api-key create --service-account-id <sa_id>

На выходе получишь длинную строку. Положи её в .env и забудь про refresh-токены — API-ключ живёт долго и обновляется руками.

Минимальный запрос на curl

YandexGPT — REST с JSON. Эндпоинт для синхронной генерации:

curl -X POST https://llm.api.cloud.yandex.net/foundationModels/v1/completion \
  -H "Content-Type: application/json" \
  -H "Authorization: Api-Key $YC_API_KEY" \
  -d '{
    "modelUri": "gpt://<FOLDER_ID>/yandexgpt/latest",
    "completionOptions": {
      "stream": false,
      "temperature": 0.3,
      "maxTokens": 1024
    },
    "messages": [
      { "role": "system", "text": "Ты лаконичный технический ассистент." },
      { "role": "user", "text": "Что такое индекс GIN в Postgres?" }
    ]
  }'

Поле modelUri — это ключевое отличие от OpenAI-подобных API: модель указывается через URI с твоим folder_id. Возможные модели: yandexgpt/latest (Pro), yandexgpt-lite/latest (Lite, быстрее и дешевле), yandexgpt-32k/latest, yandexgpt-5-pro/latest — точные имена смотри в Console > Foundation Models. Версии и цены меняются, имена остаются стабильными.

Клиент на Node

SDK для YandexGPT в виде официального npm-пакета у Яндекса нет (есть только Python и неофициальные обёртки). Я обычно беру нативный fetch — этого хватает с головой.

type YGptMessage = {
  role: "system" | "user" | "assistant";
  text: string;
};

type YGptOptions = {
  temperature?: number;
  maxTokens?: number;
  model?: string; // "yandexgpt" | "yandexgpt-lite" | "yandexgpt-5-pro"
};

async function ygpt(
  messages: YGptMessage[],
  options: YGptOptions = {},
) {
  const folderId = process.env.YC_FOLDER_ID!;
  const apiKey = process.env.YC_API_KEY!;
  const model = options.model ?? "yandexgpt";

  const res = await fetch(
    "https://llm.api.cloud.yandex.net/foundationModels/v1/completion",
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Api-Key ${apiKey}`,
      },
      body: JSON.stringify({
        modelUri: `gpt://${folderId}/${model}/latest`,
        completionOptions: {
          stream: false,
          temperature: options.temperature ?? 0.3,
          maxTokens: options.maxTokens ?? 2000,
        },
        messages,
      }),
    },
  );

  if (!res.ok) {
    throw new Error(`YGPT ${res.status}: ${await res.text()}`);
  }

  const data = await res.json();
  const text: string = data.result.alternatives[0].message.text;
  const usage = data.result.usage;
  return { text, usage };
}

Поля в ответе нестандартные — result.alternatives[0].message.text. Когда впервые лезешь после OpenAI или Anthropic, это сбивает. usage возвращает inputTextTokens, completionTokens, totalTokens в виде строк (да, строк), так что для математики кастуй в Number.

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

YandexGPT поддерживает стрим — параметр stream: true в completionOptions. Формат — не SSE и не chunked-JSON стандарта OpenAI, а свой: серия JSON-объектов, разделённых переводом строки. Каждый объект — это полный накопленный текст, а не дельта.

async function* ygptStream(messages: YGptMessage[]) {
  const folderId = process.env.YC_FOLDER_ID!;
  const apiKey = process.env.YC_API_KEY!;

  const res = await fetch(
    "https://llm.api.cloud.yandex.net/foundationModels/v1/completion",
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Api-Key ${apiKey}`,
      },
      body: JSON.stringify({
        modelUri: `gpt://${folderId}/yandexgpt/latest`,
        completionOptions: { stream: true, temperature: 0.3, maxTokens: 2000 },
        messages,
      }),
    },
  );

  if (!res.body) throw new Error("no stream body");
  const reader = res.body.getReader();
  const decoder = new TextDecoder();
  let buffer = "";
  let prev = "";

  while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });

    const lines = buffer.split("\n");
    buffer = lines.pop() || "";

    for (const line of lines) {
      if (!line.trim()) continue;
      const obj = JSON.parse(line);
      const text: string = obj.result.alternatives[0].message.text;
      const delta = text.slice(prev.length);
      prev = text;
      if (delta) yield delta;
    }
  }
}

Главный нюанс: чтобы получить дельту (для пробрасывания в UI), считаешь её сам — отнимаешь предыдущую длину от новой. Это не самый удобный API, но работает стабильно.

Эмбеддинги

YandexGPT даёт два эмбеддинга через foundationModels/v1/textEmbedding: doc (для индексации) и query (для поиска). Размерность — 256.

async function embed(text: string, type: "doc" | "query" = "doc") {
  const folderId = process.env.YC_FOLDER_ID!;
  const apiKey = process.env.YC_API_KEY!;

  const res = await fetch(
    "https://llm.api.cloud.yandex.net/foundationModels/v1/textEmbedding",
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Api-Key ${apiKey}`,
      },
      body: JSON.stringify({
        modelUri: `emb://${folderId}/text-search-${type}/latest`,
        text,
      }),
    },
  );
  const data = await res.json();
  return data.embedding as number[];
}

Использовать тот же тип для индекса и для запроса — нельзя, иначе совпадения хуже. Это асимметричная модель: документы и запросы живут в разных эмбеддинг-пространствах.

Грабли, на которые я наступал

Кириллические токены. Один русский символ — примерно один токен. Контекст-окно у Pro — 8000 токенов, у 32k — 32000. Большая разница, выбирай модель по реальному размеру входа. У 5 Pro — 32000 токенов и заметно лучше качество.

Лимиты RPS. На каталог по умолчанию даётся скромный лимит запросов в секунду. Если дёргаешь API из батча — получишь 429. Поднимать лимит можно тикетом в поддержку, но проще — стейтлесс ретраи с экспоненциальной паузой.

Биллинг по токенам, но округление вверх по запросу. Пара кратких "привет"-запросов всё равно списывает минимум. Для прода это не страшно, но если ты гоняешь юнит-тесты на боевом ключе — заведи отдельный folder с маленькой смешной квотой.

Ошибка 429 "resource.usage exhausted" в начале месяца. Это значит, что у тебя стоит автоматический лимит на каталог. Подними его в Console → Quotas. Несколько раз я тратил час, думая, что у меня баг в коде.

Контентные фильтры. Yandex применяет фильтры на ввод и вывод. Иногда модель отказывается отвечать на "чувствительные" темы, даже когда их там нет с точки зрения здравого смысла. Это особенность сервиса. Заранее закладывай fallback на "модель отказалась — покажи статичный текст".

Когда YandexGPT — правильный выбор

Если у тебя B2B-клиент в России, требование "данные не уходят за рубеж", и задачи бытового LLM-уровня — суммаризация, классификация, ответы по корпоративной базе с RAG — YandexGPT справляется. Pro по качеству — на уровне середины зарубежных моделей пару поколений назад, на простых задачах разница незаметна. 5 Pro заметно подтянулся, особенно на reasoning.

Если задача — сложный код или продвинутый reasoning — Claude или OpenAI сильнее. И там, и там стек проще: SDK официальные, документация подробнее, формат ответов — стандартный.

Универсальный паттерн, к которому я пришёл: оборачивать YandexGPT в тот же интерфейс chat()/embed(), что и остальные провайдеры. Дальше в коде ты не паришься, у тебя просто provider: "yandex" в конфиге сервиса. Когда в проде один интерфейс, выбор провайдера становится не архитектурной задачей, а строкой в .env.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.