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 имеет тип объединения ваших вариантов, а обращение к несуществующему вопросу подсветит компилятор.

Источники