YandexGPT API: подключение из Node без боли
На одном из проектов я делал бота для внутренней техподдержки в большой компании, и заказчик попросил российскую 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.