Jev на JavaScript и TypeScript — SDK и примеры
Для Node.js у TypeSafe есть официальный пакет @typesafe-ai/sdk с типами TypeScript. Он выводит типы ответов из ваших вопросов, повторяет запросы при перегрузке и по умолчанию не работает в браузере, чтобы ключ не утёк. Нужен Node.js 20 или новее.
Установка
npm install @typesafe-ai/sdk
export TYPESAFE_API_KEY="ваш_ключ"
Нужен Node.js 20 или новее. В пакете есть сборки ESM и CommonJS и объявления типов TypeScript. Актуальная версия на момент написания — 0.6.0. В ней уровни Score передаются упорядоченным массивом, а не объектом с числовыми ключами, поэтому код для 0.5.x со старым форматом придётся поправить.
Клиент читает ключ из TYPESAFE_API_KEY. Если ключа нет ни в настройках, ни в окружении, конструктор сразу выбросит TypeSafeError, и ошибка всплывёт при старте приложения, а не на первом запросе.
Первый запрос
import { choice, TypeSafeClient } from "@typesafe-ai/sdk";
const client = new TypeSafeClient();
const response = await client.systemOne({
state: { document: "С карты списали оплату дважды. Верните деньги." },
questions: {
category: choice("О чём обращение?", {
billing: "Оплата, списания, возвраты",
technical: "Ошибки и сбои",
other: null,
}),
},
});
console.log(response.answers.category.choice); // "billing"
console.log(response.model, response.usage.input_tokens);
systemOne принимает объект с полями state, questions и необязательным model. Ответы приходят в response.answers под теми же ключами, что и вопросы, рядом лежат model с версией модели и usage с токенами.
Главное удобство SDK — типы. Поле response.answers.category.choice имеет тип "billing" | "technical" | "other", поэтому сравнение с опечаткой вроде === "biling" компилятор отметит как ошибку. Модель по умолчанию — jev-latest. Поменять её можно полем model в запросе или параметром defaultModel в настройках клиента.
Хелперы choice, noul и score
| Хелпер | Аргументы | Поля ответа |
|---|---|---|
noul(instructions, criteria?) |
Вопрос и необязательные описания true и false |
noul |
choice(instructions, criteria) |
Вопрос и объект «вариант → описание или null» | choice, probabilities, confidence |
score(instructions, criteria) |
Вопрос и массив минимум из двух уровней | score, legend, probabilities, confidence |
import { choice, noul, score, TypeSafeClient } from "@typesafe-ai/sdk";
const client = new TypeSafeClient();
const { answers } = await client.systemOne({
state: { ticket: "Третий день не проходят платежи клиентов, теряем заказы!" },
questions: {
urgent: noul("Клиент сообщает о срочной проблеме?", {
true: "Проблема мешает работе прямо сейчас",
false: "Вопрос может подождать",
}),
team: choice("Какая команда должна заняться обращением?", {
billing: "Платежи, счета, возвраты",
technical: "Ошибки, сбои, интеграции",
}),
frustration: score("Насколько раздражён клиент?", [
"Спокоен",
"Раздражён, но вежлив",
"Очень зол, резкие выражения",
]),
},
});
if (answers.urgent.noul > 0.8 && answers.team.confidence >= 0.6) {
await assign(answers.team.choice, "high"); // assign — ваша функция маршрутизации
}
console.log(answers.frustration.score, answers.frustration.probabilities);
Все три вопроса уходят одним запросом и считаются параллельно, так что третий вопрос почти не добавляет задержки. Хелперы — просто удобная запись: вопрос можно передать и объектом в формате API, например { type: "noul", instructions: "Это спам?" }. Если вопросов нет или у шкалы меньше двух уровней, systemOne выбросит ошибку ещё до отправки.
score — дробная позиция на шкале, а не номер уровня. Как читать это поле и confidence, объясняет справочник по API, а как выбирать тип вопроса — статья о Choice, Score и Noul. Пороги 0,8 и 0,6 в примере условные.
Настройки клиента
| Параметр | Что задаёт | По умолчанию |
|---|---|---|
apiKey |
Ключ | TYPESAFE_API_KEY |
baseURL |
Адрес API | TYPESAFE_BASE_URL, затем https://api.typesafe.ai |
defaultModel |
Модель | TYPESAFE_DEFAULT_MODEL, затем jev-latest |
timeout |
Таймаут одной попытки, мс | 10 000 |
retry |
Настройки повторов | 2 повтора |
logLevel |
Подробность логов | TYPESAFE_LOG_LEVEL, затем warn |
fetch |
Своя реализация fetch | Глобальный fetch |
dangerouslyAllowBrowser |
Разрешить работу в браузере | false |
Второй аргумент systemOne переопределяет настройки для одного вызова: timeout, retry, headers и signal для отмены через AbortController.
const r = await client.systemOne(
{ state: text, questions: { spam: noul("Это рекламный спам?") } },
{ timeout: 2000, retry: { maxRetries: 1 } },
);
Повторы и ошибки
По умолчанию SDK делает до двух повторов на ответы 408, 429 и 500–599 (529 попадает сюда же), а также при сбоях соединения и таймаутах. Пауза начинается с 500 мс и удваивается до 5000 мс со случайным разбросом. Если сервер прислал Retry-After не длиннее 60 секунд, SDK ждёт ровно столько.
Учтите отличие от Python SDK: timeout здесь действует на одну попытку, общего бюджета на все повторы нет. С настройками по умолчанию худший случай — три попытки по 10 секунд плюс паузы между ними. Для обработчиков, которые должны ответить быстро, уменьшайте timeout и maxRetries.
import { APIError, noul, RateLimitError, TypeSafeClient } from "@typesafe-ai/sdk";
const client = new TypeSafeClient();
async function isSpam(text: string): Promise<number | null> {
try {
const { answers } = await client.systemOne({
state: text,
questions: { spam: noul("Это рекламный спам?") },
});
return answers.spam.noul;
} catch (err) {
if (err instanceof RateLimitError) {
console.warn("лимит запросов, пауза", err.retryAfterMs, "мс");
return null;
}
if (err instanceof APIError) {
console.error(err.status, err.requestId, err.body);
return null;
}
throw err; // сеть, таймаут или отмена
}
}
Каждому HTTP-коду соответствует свой класс: BadRequestError (400), AuthenticationError (401), PermissionDeniedError (403), NotFoundError (404), UnprocessableEntityError (422), RateLimitError (429) и InternalServerError (5xx). Все они наследуют APIError с полями status, body и requestId. Сетевые сбои приходят как APIConnectionError и APITimeoutError, отмена через signal — как APIUserAbortError.
Если requestId нужен и при успешном ответе, вызовите client.systemOne(...).withResponse(). Метод вернёт данные, HTTP-ответ и ID запроса. Подробный разбор кодов — в статье об ошибках API Jev.
Общий дедлайн на вызов вместе со всеми повторами задают через signal: он отменяет и текущий запрос, и ожидающие повторы. По истечении срока SDK выбросит APIUserAbortError.
const r = await client.systemOne(
{ state: text, questions: { spam: noul("Это рекламный спам?") } },
{ signal: AbortSignal.timeout(5000) }, // не дольше 5 секунд на всё
);
Логи и список моделей
Параметр logLevel (или переменная TYPESAFE_LOG_LEVEL) управляет логами. На уровне info SDK пишет краткие сводки запросов, на debug добавляет заголовки и тела. Известные заголовки с ключами маскируются, тела запросов — нет, поэтому с персональными данными в state уровень debug в продакшене лучше не включать.
client.models.list() возвращает модели, доступные аккаунту: имя, описание и дату выпуска. Сейчас в списке алиасы jev-latest и jev-preview, а точную версию jev-1.13.0 API принимает, даже если её нет в списке. Если вы подбирали пороги под конкретную версию, зафиксируйте её через defaultModel.
Пример для Next.js: API-роут модерации
// app/api/moderate/route.ts
import { noul, TypeSafeClient } from "@typesafe-ai/sdk";
const client = new TypeSafeClient(); // один клиент на процесс
export async function POST(req: Request) {
const { text } = await req.json();
const { answers } = await client.systemOne({
state: { comment: text },
questions: {
toxic: noul("Комментарий оскорбительный или токсичный?"),
},
});
const p = answers.toxic.noul;
const decision = p < 0.3 ? "publish" : p < 0.8 ? "review" : "reject";
return Response.json({ decision, p });
}
Клиент создаётся один раз на уровне модуля и переиспользуется между запросами. Ключ остаётся на сервере, браузер получает только решение. Вместо одного порога здесь три зоны: публиковать сразу, отправить модератору, отклонить. Границы 0,3 и 0,8 условные, их нужно подобрать на своих данных. Спам, рекламу и персональные данные проверяйте в том же запросе отдельными вопросами. Полный сценарий разобран в статье о модерации комментариев.
Почему не из браузера
В браузере конструктор TypeSafeClient по умолчанию выбрасывает ошибку: ключ API оказался бы в коде страницы, и его забрал бы любой посетитель. Флаг dangerouslyAllowBrowser: true снимает запрет, но саму проблему не решает. Вызывайте Jev из API-роута, серверной функции или воркера.
Частые ошибки
- Ключ в переменной
NEXT_PUBLIC_…. Next.js встраивает такие переменные в клиентский бандл. Храните ключ в обычной серверной переменной, напримерTYPESAFE_API_KEY. - Новый клиент в каждом обработчике. Конструктор проверяет ключ и настройки, поэтому создавайте клиент один раз на уровне модуля и переиспользуйте его.
/v1вbaseURL. SDK сам добавляет/v1/systemone, и адрес с/v1на конце даст путь с двойным/v1.- Шкала объектом. С версии 0.6.0 уровни Score передаются массивом. Объект с ключами
0,1,2из старых примеров не пройдёт. - Сравнение
scoreчерез===.scoreдробный, поэтомуanswers.frustration.score === 2почти никогда не сработает. Сравнивайте с порогом или округляйте черезMath.round.
Через OpenRouter и Polza.AI
const openrouter = new TypeSafeClient({
apiKey: process.env.OPENROUTER_API_KEY,
baseURL: "https://openrouter.ai/api",
});
const polza = new TypeSafeClient({
apiKey: process.env.POLZA_AI_API_KEY,
baseURL: "https://polza.ai/api",
defaultModel: "typesafe/jev",
});
baseURL указывается без /v1, путь /v1/systemone SDK добавит сам. OpenRouter принимает и модель по умолчанию jev-latest, и точное имя typesafe/jev-1.13, подробнее — в статье о Jev через OpenRouter. Polza.AI возвращает стоимость запроса в рублях в поле usage.cost_rub, и, по данным Polza.AI, TypeScript SDK его сохраняет. Подключение из России описано в статье о Jev через Polza.AI.
Без SDK: обычный fetch
Подходит для Cloudflare Workers, Deno и проектов, куда не хочется добавлять зависимости:
const res = await fetch("https://api.typesafe.ai/v1/systemone", {
method: "POST",
headers: {
Authorization: `Bearer ${env.TYPESAFE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "jev-latest",
state: "Текст для оценки",
questions: { spam: { type: "noul", instructions: "Это рекламный спам?" } },
}),
});
if (!res.ok) throw new Error(`Jev: ${res.status} ${await res.text()}`);
const data = await res.json();
const spam = data.answers.spam.noul;
Без SDK повторы ложатся на вас: на 429 и 529 повторяйте запрос с растущей паузой. Если проект уже работает на Cloudflare, сравните этот способ с вызовом Jev через Workers AI. Готовый код под ваши вопросы собирается в конструкторе запроса.
Частые вопросы
Как установить SDK Jev для Node.js?
Командой npm install @typesafe-ai/sdk. Нужен Node.js 20 или новее. Пакет поддерживает ESM и CommonJS и содержит типы TypeScript.
Можно ли вызывать Jev из браузера?
По умолчанию SDK это запрещает и выбрасывает ошибку, чтобы ключ API не попал в код страницы. Вызывайте Jev с сервера, а браузеру отдавайте только результат.
Как подключить JavaScript SDK к OpenRouter или Polza.AI?
Передайте в конструктор apiKey площадки и baseURL без /v1, например https://openrouter.ai/api или https://polza.ai/api. Для Polza.AI задайте ещё модель typesafe/jev.
Повторяет ли SDK запросы при ошибках?
Да. По умолчанию до двух повторов на ответы 408, 429 и 5xx с экспоненциальной паузой от 500 мс. Поведение меняется параметром retry.
Знает ли TypeScript типы ответов?
Да. SDK выводит их из вопросов, поэтому поле choice имеет тип объединения ваших вариантов, а обращение к несуществующему вопросу подсветит компилятор.