Jev на Python — установка SDK и примеры
Официальный пакет typesafe-sdk — самый короткий путь от ключа до работающей интеграции. Он типизирует вопросы и ответы и сам повторяет запросы при перегрузке. Примеры и настройки ниже сверены с документацией SDK версии 0.7.1.
Установка и ключ
pip install typesafe-sdk # или: uv add typesafe-sdk
export TYPESAFE_API_KEY="ваш_ключ"
Нужен Python 3.10 или новее. Пакет называется typesafe-sdk, а импортируется как typesafe_sdk. Ключ SDK берёт из переменной TYPESAFE_API_KEY, если не передать его в клиент явно. Пробелы и переводы строк по краям ключа отбрасываются. Пустой ключ или ключ с недопустимыми символами клиент отклонит ещё при создании: конструктор выбросит TypeSafeError до первого запроса.
Проверьте версию пакета. В 0.6.0 уровни Score стали передаваться упорядоченным списком вместо словаря с числовыми ключами, а в 0.7.0 SDK перешёл с msgspec на pydantic. Код для версий до 0.6.0 со словарём уровней придётся переписать.
| Переменная | Что задаёт | По умолчанию |
|---|---|---|
TYPESAFE_API_KEY |
Ключ API | — |
TYPESAFE_BASE_URL |
Адрес API | https://api.typesafe.ai |
TYPESAFE_DEFAULT_MODEL |
Модель | jev-latest |
TYPESAFE_LOG_LEVEL |
Уровень логов typesafe_sdk |
не задан |
Явные параметры клиента важнее переменных окружения.
Первый запрос
from typesafe_sdk import Choice, Noul, Score, TypeSafeClient
with TypeSafeClient() as client:
response = client.system_one(
state={"document": "С карты списали оплату дважды. Верните деньги срочно."},
questions={
"billing": Noul(instructions="Обращение касается оплаты?"),
"tone": Choice(
instructions="Какой тон у клиента?",
criteria={"calm": None, "frustrated": None, "angry": None},
),
"urgency": Score(
instructions="Насколько срочно нужно ответить?",
criteria=["Может подождать неделю", "Ответить за день", "Ответить за час"],
),
},
)
print(response.nouls["billing"].noul) # 0.97
print(response.choices["tone"].choice) # frustrated
print(response.scores["urgency"].score) # 1.62
print(response.model, response.usage.input_tokens)
Числа в комментариях условные. Разберём, что здесь происходит.
withзакрывает соединения при выходе из блока. Без него вызовитеclient.close()сами.stateпринимает строку, словарь или список. Вопросы передаются словарём «ключ → вопрос». ОбъектыNoul,ChoiceиScoreпроверяют поля до отправки, но SDK примет и обычные словари в формате API вроде{"type": "noul", "instructions": "..."}, их можно смешивать.- Ответы лежат в
response.answersпо ключам вопросов. Свойстваresponse.nouls,response.choicesиresponse.scoresдают те же ответы, сгруппированные по типам, так что IDE подсказывает поля. - Уровни Score в SDK индексируются целыми числами:
probabilities[2]иlegend[2], а не строковыми ключами, как в JSON. - Метаданные:
response.model— версия модели,response.usage— токены,response.request_id— ID запроса из заголовкаx-typesafe-request-id. Сохраняйте его в логах, он понадобится при разборе проблем.
Модель по умолчанию — jev-latest. Зафиксировать версию можно в клиенте, TypeSafeClient(model="jev-1.13.0"), или в отдельном вызове: client.system_one(state, questions, model="jev-1.13.0"). Как выбирать типы вопросов и писать уровни, разобрано в статье о Choice, Score и Noul, а все поля ответа описаны в справочнике по API.
Асинхронный клиент: пачка документов
import asyncio
from typesafe_sdk import AsyncTypeSafeClient, Noul
TICKETS = ["Не могу войти в аккаунт", "Спасибо, всё работает!", "Верните деньги!!!"]
async def classify(client, limit, text):
async with limit:
r = await client.system_one(
state={"ticket": text},
questions={"complaint": Noul(instructions="Клиент жалуется на проблему?")},
)
return text, r.nouls["complaint"].noul
async def main():
limit = asyncio.Semaphore(5) # 5 одновременно ≈ 1000 запросов в минуту при ответе за 0,3 с
async with AsyncTypeSafeClient() as client:
results = await asyncio.gather(*(classify(client, limit, t) for t in TICKETS))
for text, p in results:
print(f"{p:.2f} {text}")
asyncio.run(main())
У AsyncTypeSafeClient те же параметры, что у синхронного клиента, только system_one вызывается через await. Создайте один клиент на всю пачку и ограничьте число одновременных запросов семафором. Учтите, что семафор ограничивает одновременность, а не частоту: при ответе за 0,3 секунды каждый поток даёт около 200 запросов в минуту, поэтому 5 потоков — это примерно 1000 в минуту, с запасом под лимит. Без ограничения тысяча документов уйдёт разом, упрётся в лимиты 1200 запросов в минуту и 250 000 токенов в секунду и получит ошибки 429. SDK их повторит, но пачка обработается медленнее, чем при ровном потоке. Если к каждому документу несколько вопросов, задавайте их одним запросом, а не несколькими. Как выдерживать темп, настраивать повторы и разбирать упавшие документы, рассказано в статье об асинхронном клиенте.
Типизированный ответ
from typesafe_sdk import Noul, NoulAnswer, SystemOneResponse, TypeSafeClient
class BillingResponse(SystemOneResponse):
billing: NoulAnswer
with TypeSafeClient() as client:
result = client.system_one(
"С карты дважды списали оплату.",
{"billing": Noul(instructions="Обращение касается оплаты?")},
response_model=BillingResponse,
)
print(result.billing.noul)
Параметр response_model появился в версии 0.7.0. Вы описываете ожидаемые ответы моделью pydantic и обращаетесь к ним как к атрибутам с подсказками IDE. Если ответ не совпал с описанием, SDK выбросит TypeSafeAPIResponseValidationError с путём к проблемному полю.
Маршрутизация по уверенности
p = response.nouls["billing"].noul
if p >= 0.85:
route_to("billing")
elif p <= 0.15:
route_to("general")
else:
route_to("human_review") # серая зона
Пороги 0,85 и 0,15 здесь условные. У Choice и Score в ответе есть ещё confidence, и по нему строят такие же три ветки: действовать, перепроверить, отдать человеку. Как подобрать пороги на размеченной выборке, рассказано в статье об уверенности Jev.
Повторы, таймауты и ошибки
По умолчанию SDK повторяет запрос до двух раз на ответы 408, 429 и любые 5xx, включая 529, а также при обрыве соединения и таймауте. Пауза начинается с 0,5 секунды и удваивается до 5 секунд со случайным разбросом, заголовок Retry-After учитывается. На одну HTTP-операцию отводится 10 секунд, на весь вызов вместе с повторами — 30.
from typesafe_sdk import RetryPolicy, TypeSafeClient
client = TypeSafeClient(
timeout=3.0, # секунд на одну HTTP-операцию
retry=RetryPolicy(max_retries=4, backoff_max=2.0, timeout=15.0),
)
RetryPolicy(max_retries=0) отключает повторы. Политику и таймаут можно передать и в отдельный вызов: client.system_one(state, questions, retry=..., timeout=...).
Если запрос так и не прошёл, SDK выбросит исключение:
| Исключение | Когда |
|---|---|
TypeSafeAuthenticationError |
401, неверный ключ |
TypeSafeUnprocessableEntityError |
422, запрос не прошёл валидацию |
TypeSafeRateLimitError |
429 после всех повторов, в retry_after_ms подсказка о паузе |
TypeSafeInternalServerError |
Ошибки 5xx, в том числе 529 |
TypeSafeAPIConnectionError, TypeSafeAPITimeoutError |
Нет соединения или истёк таймаут |
HTTP-ошибки наследуют TypeSafeAPIError с полями status, body и request_id, а все исключения SDK — TypeSafeError.
import logging
from typesafe_sdk import TypeSafeAPIError, TypeSafeRateLimitError
log = logging.getLogger(__name__)
try:
response = client.system_one(state, questions)
except TypeSafeRateLimitError as e:
log.warning("лимит запросов, пауза %s мс", e.retry_after_ms)
except TypeSafeAPIError as e:
log.error("ошибка %s, request_id=%s, тело: %s", e.status, e.request_id, e.body)
Что означает каждый код и как строить повторы без SDK, описано в статье об ошибках API Jev.
Подключение через OpenRouter, Polza.AI и Vercel
Адрес API задаёт параметр base_url или переменная TYPESAFE_BASE_URL. Указывайте корень без /v1: путь /v1/systemone SDK допишет сам.
import os
from typesafe_sdk import TypeSafeClient
openrouter = TypeSafeClient(
api_key=os.environ["OPENROUTER_API_KEY"],
base_url="https://openrouter.ai/api",
model="typesafe/jev-1.13",
)
polza = TypeSafeClient(
api_key=os.environ["POLZA_AI_API_KEY"],
base_url="https://polza.ai/api",
model="typesafe/jev",
)
vercel = TypeSafeClient(
api_key=os.environ["AI_GATEWAY_API_KEY"],
base_url="https://ai-gateway.vercel.sh/typesafe",
model="typesafe-ai/jev",
)
У каждой площадки свои нюансы:
- OpenRouter понимает и имена TypeSafe без префикса:
jev-latestон направит на~typesafe/jev-latest,jev-1.13— наtypesafe/jev-1.13. Список моделей черезclient.models.list()с OpenRouter не работает, потому что OpenRouter отвечает в своём формате и SDK его отклоняет. Подробности — в статье о Jev через OpenRouter. - Polza.AI возвращает стоимость запроса в рублях в поле
usage.cost_rub, но Python SDK, по данным Polza.AI, это поле отбрасывает. Достать его можно из исходного HTTP-ответа:response.raw_http_response.json()["usage"]. Как подключиться и оплатить из России, читайте в статье о Jev через Polza.AI. - Vercel AI Gateway работает с ключом шлюза и оплачивается кредитами Vercel. Настройка описана в статье о Jev в Vercel AI Gateway.
Частые ошибки
- Новый клиент на каждый запрос. Клиент держит HTTP-соединения, поэтому создавайте его один раз при старте приложения и переиспользуйте.
- Синхронный клиент внутри async-кода.
TypeSafeClientблокирует цикл событий на время запроса. В FastAPI, aiohttp и других асинхронных фреймворках беритеAsyncTypeSafeClient. /v1вbase_url. SDK сам добавляет/v1/systemone, поэтому адрес видаhttps://polza.ai/api/v1превратится в путь с двойным/v1.- Пустые вопросы и пустая шкала. Если словарь вопросов пуст или у Score нет уровней, SDK выбросит
TypeSafeErrorещё до отправки запроса. - Поле не того типа. У ответа Noul нет
confidenceиchoice, толькоnoul. Читайте ответы черезresponse.nouls,response.choicesиresponse.scores: у каждого из них свой тип ответа, и IDE подскажет, какие поля доступны.
Логирование
import logging
logging.getLogger("typesafe_sdk").setLevel(logging.INFO)
На уровне info SDK пишет одну строку на запрос, на debug добавляет заголовки и тела запросов и ответов. Ключи и другие секретные заголовки в логах маскируются, а тела — нет. Если в state бывают персональные данные, не включайте debug в продакшене. Без правки кода уровень задаёт переменная TYPESAFE_LOG_LEVEL со значением debug, info, warning, error или off, её нужно выставить до импорта SDK.
Частые вопросы
Как называется пакет Jev для Python?
typesafe-sdk. Установка — pip install typesafe-sdk или uv add typesafe-sdk, импорт — from typesafe_sdk import TypeSafeClient. Нужен Python 3.10 или новее.
Откуда SDK берёт ключ и адрес API?
Из переменных окружения TYPESAFE_API_KEY и TYPESAFE_BASE_URL. Параметры api_key и base_url, переданные в клиент явно, важнее переменных.
Как подключить Python SDK к OpenRouter или Polza.AI?
Передайте в клиент base_url площадки без /v1 и её ключ в api_key. Для OpenRouter это https://openrouter.ai/api, для Polza.AI — https://polza.ai/api и модель typesafe/jev.
Есть ли асинхронный клиент?
Да, AsyncTypeSafeClient. У него те же параметры, что у синхронного, а system_one вызывается через await. Он удобен для параллельной обработки пачек документов.
Повторяет ли SDK запросы при ошибках?
Да. По умолчанию до двух повторов на ответы 408, 429 и 5xx с экспоненциальной паузой. Поведение настраивается через RetryPolicy.