Jev: модель решений (классификация без генерации текста)

Jev — модель решений разработчика TypeSafe. Это не чат-модель: она не пишет текст, а отвечает на заранее заданные вопросы о присланных данных — вероятностями. Вы передаёте «состояние» (state — текст или JSON) и типизированные вопросы к нему; для каждого вопроса в ответ приходит число: вероятность «да», выбранный вариант с распределением вероятностей или уровень по шкале. Результат сразу готов к использованию в коде — его не нужно вытаскивать из свободного текста.

Для чего подходит: классификация и тегирование (тема обращения, категория товара), модерация, маршрутизация запросов (в какой отдел или к какой модели отправить), решения в агентах (эскалировать ли, нужен ли человек), пакетная разметка данных. Отвечает быстро — по заявлению TypeSafe, за 70–500 мс.

Jev доступен только через отдельный эндпоинт POST /v1/systemone. В чате кабинета его нет, а запрос к нему через /v1/chat/completions вернёт ошибку.

Когда брать Jev вместо языковой модели

Задача Что выбрать
Отнести текст к одной из категорий, ответить «да/нет», оценить по шкале Jev — дешевле, быстрее и без разбора свободного текста
Разметить тысячи записей по одним и тем же вопросам Jev — пакетная обработка стоит копейки
Решить в агенте, что делать дальше: эскалировать, уточнить, ответить Jev принимает решение, затем языковая модель пишет ответ
Написать или переписать текст, перевести, резюмировать языковая модель — Chat Completions
Точный счёт, математика, сравнение дат, многоходовые рассуждения языковая модель — Jev для этого не предназначен

Лучше всего Jev работает с английским текстом. Русский понимает, но точность ниже — проверьте модель на своих примерах, прежде чем запускать в работу. Вопросы и описания вариантов можно писать на русском.

Цена

8 ₽ за 1 млн входных токенов, выходные токены бесплатны. Входные токены — это state и все вопросы вместе; их число возвращается в поле usage.input_tokens ответа.

Пример: 1 000 запросов по 300 токенов — это 300 000 токенов, то есть ≈ 2,4 ₽.

Списание — с того же баланса, что и у остальных моделей, по факту каждого запроса; запрос с ошибкой не оплачивается. В разделе «История» кабинета запросы к Jev видны как модель typesafe/jev-1.14. Цена также указана на странице цен.

Эндпоинт и аутентификация

POST https://router.1-lab.online/v1/systemone — формат запроса и ответа совпадает с API TypeSafe (System One). Ключ — обычный ключ API из раздела «Ключи» (см. Аутентификация и ключи):

Authorization: Bearer sk-...
Content-Type: application/json

Имена модели для параметра model:

Имя Что это
jev-latest актуальная версия, сейчас typesafe/jev-1.14
jev-1.14, typesafe/jev-1.14 версия 1.14 — единственная, которая принимает изображения (вопросы box)
jev-1.13, typesafe/jev-1.13 предыдущая версия
typesafe/jev то же, что jev-latest

Формат запроса

Поле Что задаёт
model имя модели (обязательное)
state данные, о которых задаются вопросы: строка, JSON-объект или массив (обязательное)
questions объект «имя вопроса → вопрос»; имена придумываете вы, под ними же придут ответы (обязательное, не меньше одного вопроса)
assets изображения в base64 для вопросов box (необязательное)

Вопрос — объект с полями type (noul, choice, score или box), instructions (сам вопрос) и, для choice и score, criteria. Несколько вопросов к одному state задавайте в одном запросе: state при этом оплачивается один раз.

{
  "model": "jev-latest",
  "state": "I was charged twice for my subscription this month. Please refund the extra charge.",
  "questions": {
    "billing": {
      "type": "noul",
      "instructions": "Is this message about billing?"
    },
    "tone": {
      "type": "choice",
      "instructions": "What is the tone of this message?",
      "criteria": {
        "angry": "An upset or hostile message",
        "calm": "A neutral or polite message",
        "excited": "An enthusiastic or eager message"
      }
    },
    "urgency": {
      "type": "score",
      "instructions": "How urgent is this message?",
      "criteria": ["Can wait", "Needs attention this week", "Needs attention today"]
    }
  }
}

Типы вопросов

noul — да или нет

Вероятность ответа «да» от 0 до 1 в поле noul. Порог выбираете сами: например, считать «да» при noul >= 0.7, а значения около 0,5 отправлять на проверку человеку.

{"type": "noul", "instructions": "Is this message about billing?"}

Ответ: {"type": "noul", "noul": 0.98}.

choice — выбор варианта

criteria — объект «вариант → описание, когда он подходит», от 1 до 255 вариантов. Описание можно не писать (null) — тогда модель ориентируется на имя варианта, но с описаниями точность выше. В ответе — выбранный вариант choice, уверенность confidence (0…1) и probabilities — вероятность каждого варианта (в сумме ≈ 1).

{
  "type": "choice",
  "instructions": "What is the tone of this message?",
  "criteria": {
    "angry": "An upset or hostile message",
    "calm": "A neutral or polite message",
    "excited": "An enthusiastic or eager message"
  }
}

Ответ: {"type": "choice", "choice": "calm", "confidence": 0.7, "probabilities": {"angry": 0.15, "calm": 0.8, "excited": 0.05}}.

score — уровень по шкале

criteria — массив описаний уровней, от 1 до 10; порядок задаёт шкалу: первый элемент — уровень 0, второй — 1 и так далее. В ответе score — взвешенная оценка, может быть дробной (1,7 — «ближе к уровню 2, чем к 1»), confidence, legend (какой уровень что значит) и probabilities по уровням.

{
  "type": "score",
  "instructions": "How urgent is this message?",
  "criteria": ["Can wait", "Needs attention this week", "Needs attention today"]
}

Ответ: {"type": "score", "score": 1.7, "confidence": 0.55, "legend": {"0": "Can wait", "1": "Needs attention this week", "2": "Needs attention today"}, "probabilities": {"0": 0.1, "1": 0.1, "2": 0.8}}.

box — объект на изображении

Координаты объекта, описанного в instructions, на изображении. Изображение (PNG или JPEG) передаётся в assets в base64 под придуманным вами плейсхолдером, а плейсхолдер ставится в state. Запрос отправляется с заголовком TypeSafe-Danger-Zone-Images: true и моделью jev-1.14. В ответе box — границы top, bottom, left, right долями от высоты и ширины изображения (0…1); confidence может быть null.

{
  "model": "jev-1.14",
  "state": "photo",
  "questions": {
    "damage": {"type": "box", "instructions": "The damaged area of the product"}
  },
  "assets": {
    "photo": {"type": "image", "format": "jpeg", "data_base64": "/9j/4AAQSkZJRg..."}
  }
}

Изображения принимаются только внутри JSON (base64); multipart/form-data не поддерживается — такой запрос вернёт 415.

Ответ

{
  "model": "jev-latest",
  "answers": {
    "billing": {"type": "noul", "noul": 0.98},
    "tone": {
      "type": "choice",
      "choice": "calm",
      "confidence": 0.7,
      "probabilities": {"angry": 0.15, "calm": 0.8, "excited": 0.05}
    },
    "urgency": {
      "type": "score",
      "score": 1.7,
      "confidence": 0.55,
      "legend": {"0": "Can wait", "1": "Needs attention this week", "2": "Needs attention today"},
      "probabilities": {"0": 0.1, "1": 0.1, "2": 0.8}
    }
  },
  "usage": {"input_tokens": 120, "output_tokens": 12}
}

answers — ответы под теми же именами, что в questions; у каждого в type — тип вопроса. Оплачиваются только usage.input_tokens.

Примеры

cURL

curl https://router.1-lab.online/v1/systemone \
  -H "Authorization: Bearer $ROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "jev-latest",
    "state": "I was charged twice for my subscription this month. Please refund the extra charge.",
    "questions": {
      "billing": {"type": "noul", "instructions": "Is this message about billing?"},
      "tone": {
        "type": "choice",
        "instructions": "What is the tone of this message?",
        "criteria": {"angry": "An upset or hostile message", "calm": "A neutral or polite message", "excited": "An enthusiastic or eager message"}
      },
      "urgency": {
        "type": "score",
        "instructions": "How urgent is this message?",
        "criteria": ["Can wait", "Needs attention this week", "Needs attention today"]
      }
    }
  }'

Python (requests)

import os
import requests

resp = requests.post(
    "https://router.1-lab.online/v1/systemone",
    headers={"Authorization": f"Bearer {os.environ['ROUTER_API_KEY']}"},
    json={
        "model": "jev-latest",
        "state": "I was charged twice for my subscription this month. Please refund the extra charge.",
        "questions": {
            "billing": {"type": "noul", "instructions": "Is this message about billing?"},
            "tone": {
                "type": "choice",
                "instructions": "What is the tone of this message?",
                "criteria": {
                    "angry": "An upset or hostile message",
                    "calm": "A neutral or polite message",
                    "excited": "An enthusiastic or eager message",
                },
            },
            "urgency": {
                "type": "score",
                "instructions": "How urgent is this message?",
                "criteria": ["Can wait", "Needs attention this week", "Needs attention today"],
            },
        },
    },
    timeout=30,
)
resp.raise_for_status()
answers = resp.json()["answers"]

if answers["billing"]["noul"] >= 0.7:
    print("обращение о платежах, тон:", answers["tone"]["choice"], "срочность:", round(answers["urgency"]["score"], 1))

JavaScript (fetch)

const resp = await fetch("https://router.1-lab.online/v1/systemone", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ROUTER_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    model: "jev-latest",
    state: "I was charged twice for my subscription this month. Please refund the extra charge.",
    questions: {
      billing: { type: "noul", instructions: "Is this message about billing?" },
      tone: {
        type: "choice",
        instructions: "What is the tone of this message?",
        criteria: { angry: "An upset or hostile message", calm: "A neutral or polite message", excited: "An enthusiastic or eager message" },
      },
      urgency: {
        type: "score",
        instructions: "How urgent is this message?",
        criteria: ["Can wait", "Needs attention this week", "Needs attention today"],
      },
    },
  }),
});
if (!resp.ok) throw new Error(`${resp.status}: ${await resp.text()}`);

const { answers, usage } = await resp.json();
console.log(answers.tone.choice, answers.urgency.score, usage.input_tokens);

n8n

Узел HTTP Request:

Настройка Значение
Method POST
URL https://router.1-lab.online/v1/systemone
Authentication Generic Credential Type → Header Auth; в credential: Name Authorization, Value Bearer sk-... (ключ хранится в credentials n8n, а не в узле)
Send Headers Content-Type = application/json
Send Body включено; Body Content Type JSON, Specify Body Using JSON
JSON тело запроса; текст из предыдущего узла удобно подставить выражением (ниже)

Выражение в поле JSON собирает тело из данных предыдущего узла ($json.text — поле с текстом обращения):

{{ JSON.stringify({
  model: "jev-latest",
  state: $json.text,
  questions: {
    billing: { type: "noul", instructions: "Is this message about billing?" },
    tone: {
      type: "choice",
      instructions: "What is the tone of this message?",
      criteria: { angry: "An upset or hostile message", calm: "A neutral or polite message", excited: "An enthusiastic or eager message" }
    }
  }
}) }}

Дальше ответы доступны как {{ $json.answers.billing.noul }} и {{ $json.answers.tone.choice }}: узел IF сравнивает noul с порогом (например, больше 0,7), узел Switch разводит ветки по значению choice.

SDK TypeSafe

Официальные SDK TypeSafe умеют работать через наш адрес: укажите ключ Роутера и базовый адрес https://router.1-lab.online — без /v1, путь /v1/systemone SDK добавляет сам (как и SDK Anthropic). Сжатие тела запроса, которое SDK включает по умолчанию (zstd для запросов от 1 КБ), наш адрес принимает — выключать его не нужно. Модель по умолчанию в SDK — jev-latest.

Вместо параметров можно задать переменные окружения TYPESAFE_API_KEY (ключ Роутера) и TYPESAFE_BASE_URL=https://router.1-lab.online — тогда клиент создаётся без аргументов.

Python (pip install typesafe-sdk)

import os
from typesafe_sdk import Choice, Noul, Score, TypeSafeClient

with TypeSafeClient(
    api_key=os.environ["ROUTER_API_KEY"],
    base_url="https://router.1-lab.online",  # без /v1: SDK сам добавит /v1/systemone
) as client:
    response = client.system_one(
        state="I was charged twice for my subscription this month. Please refund the extra charge.",
        questions={
            "billing": Noul(instructions="Is this message about billing?"),
            "tone": Choice(
                instructions="What is the tone of this message?",
                criteria={
                    "angry": "An upset or hostile message",
                    "calm": "A neutral or polite message",
                    "excited": "An enthusiastic or eager message",
                },
            ),
            "urgency": Score(
                instructions="How urgent is this message?",
                criteria=["Can wait", "Needs attention this week", "Needs attention today"],
            ),
        },
    )

print(response.nouls["billing"].noul)
print(response.choices["tone"].choice)
print(response.scores["urgency"].score)

JavaScript / TypeScript (npm install @typesafe-ai/sdk, Node.js 22 и новее)

import { choice, noul, score, TypeSafeClient } from "@typesafe-ai/sdk";

const client = new TypeSafeClient({
  apiKey: process.env.ROUTER_API_KEY,
  baseURL: "https://router.1-lab.online", // без /v1: SDK сам добавит /v1/systemone
});

const { answers, usage } = await client.systemOne({
  state: "I was charged twice for my subscription this month. Please refund the extra charge.",
  questions: {
    billing: noul("Is this message about billing?"),
    tone: choice("What is the tone of this message?", {
      angry: "An upset or hostile message",
      calm: "A neutral or polite message",
      excited: "An enthusiastic or eager message",
    }),
    urgency: score("How urgent is this message?", ["Can wait", "Needs attention this week", "Needs attention today"]),
  },
});
console.log(answers.billing.noul, answers.tone.choice, answers.urgency.score, usage.input_tokens);

Ограничения

Что Значение
Объём запроса state и вопросы вместе — до 64 тыс. токенов; с заголовком TypeSafe-Danger-Zone-Long-Context: true — до 262 тыс.
Длинный контекст для state больше ~32 000 байт добавьте заголовок TypeSafe-Danger-Zone-Long-Context: true; такие запросы медленнее и менее точны
Вариантов в choice от 1 до 255
Уровней в score от 1 до 10
Изображения только jev-1.14; PNG или JPEG в base64 в поле assets, заголовок TypeSafe-Danger-Zone-Images: true
Формат запроса только JSON; multipart/form-data не принимается (415)
Задержка ответа 70–500 мс по данным TypeSafe
Чат и Chat Completions недоступны: только POST /v1/systemone

Ошибки

Формат ошибок отличается от остальных эндпоинтов: вместо объекта error — поле detail.

Код Тело Что случилось Что делать
400 {"detail": "..."} запрос не прошёл проверку: неизвестная модель, слишком много вариантов или уровней, изображение без заголовка исправьте запрос по тексту detail
401 {"error": {"message": "Invalid token", …}} ключ не передан, неверный, выключен или удалён проверьте ключ в разделе «Ключи»
403 {"error": {"message": "… quota is not enough …", "code": "insufficient_user_quota"}} на балансе недостаточно денег или исчерпан лимит расходов ключа; те же коды, что у остальных эндпоинтов пополните баланс или поднимите лимит ключа
415 тело не JSON — например, multipart/form-data отправьте JSON; изображения — в assets в base64
422 {"detail": [...]} тело не соответствует схеме: нет обязательного поля, не тот тип значения, неверный type вопроса в каждом элементе списка detail — путь до поля (loc) и сообщение (msg)
429 слишком много запросов за короткое время подождите, сколько указано в заголовке Retry-After, и повторите
502 модель не ответила или ответила с ошибкой повторите через несколько секунд

Повторять можно 429, 502 и сетевые сбои — с нарастающей паузой, как описано в разделе Ошибки и лимиты. Ошибки 400, 401, 403, 415 и 422 повтор не исправит — нужно поменять запрос, ключ или пополнить баланс. Запрос с ошибкой не оплачивается.

Советы

  • Задавайте все вопросы к одному тексту в одном запросе: state оплачивается один раз, а не на каждый вопрос.
  • Описывайте варианты в criteria — одно-два предложения о том, когда вариант подходит, заметно повышают точность.
  • Используйте confidence и probabilities: уверенные ответы обрабатывайте автоматически, неуверенные — отправляйте человеку.
  • Формулируйте вопросы и критерии по-английски, если это возможно; для русских текстов проверьте качество на 20–50 реальных примерах.
  • Сочетайте модели: Jev решает, что делать с обращением, а языковая модель пишет ответ клиенту.