RU ▾
Получить API-ключ

NsfwAiApiРуководство

NSFW LLM API в Python: потоковая передача, JSON и инструменты, JSON и инструменты

Чтобы использовать NSFW LLM API из Python, вам нужен только стандартный пакет openai и два изменения: базовый URL и ключ. Это руководство показывает первый запрос, потоковую передачу, JSON-режим, вызов инструментов и аккуратную обработку лимитов запросов.

Обновлено

Окно терминала, испускающее поток светящихся токенов, обернутых спиральной лентой света

Настройка: установка и первый вызов

Вам нужен актуальный Python 3, пакет openai (pip install openai) и API-ключ. Новые аккаунты могут получить $0,50 пробного баланса на странице API-ключа одним кликом после входа; для этого не нужны данные карты. Храните ключ в переменной окружения, а не в исходном коде.

Сервис использует протокол чата OpenAI: POST /v1/chat/completions для генерации и GET /v1/models для получения списка моделей. Аутентификация — через токен Bearer, а идентификатор модели — uncensored. Вот минимальный рабочий скрипт:

import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.nsfwaiapi.com/v1",
    api_key=os.environ["NSFWAIAPI_KEY"],
)

resp = client.chat.completions.create(
    model="uncensored",
    messages=[
        {"role": "system", "content": "You are a fiction co-writer for adult readers. Write vivid, non-explicit scenes."},
        {"role": "user", "content": "Open a noir story in a rainy harbor town, three sentences."},
    ],
    temperature=0.8,
)

print(resp.choices[0].message.content)
print(resp.usage)

Если вы предпочитаете сначала проверить соединение из оболочки, тот же запрос работает с curl:

curl https://api.nsfwaiapi.com/v1/chat/completions \
  -H "Authorization: Bearer $NSFWAIAPI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "uncensored", "messages": [{"role": "user", "content": "Say hello in five words."}]}'

Модель настроена на ответы без отказов по контенту для законного использования взрослыми. Это влияет на тон того, что она напишет, но не на структуру API, поэтому остальная часть вашего кода выглядит как любая другая интеграция с OpenAI. Используйте промпты для художественной литературы, чата с персонажами и других законных проектов для взрослых и убедитесь, что все персонажи — взрослые.

Несколько привычек помогут с первого дня. Поместите инструкции, которые никогда не меняются, в системное сообщение, а пользовательское сообщение оставьте для части, которая меняется каждый ход. Выберите одну температуру для задачи и не меняйте её, пока сравниваете промпты, потому что изменение двух вещей сразу ничего вам не даст. Логируйте идентификатор запроса и количество токенов из resp.usage рядом с каждым вызовом, чтобы, когда ваш счет или вывод удивят вас, был журнал для проверки.

Потоковая передача ответов с помощью SSE

Установите stream=True, и сервер отправляет токены как события, отправляемые сервером. Для чат-приложений это имеет большое значение, потому что читатель видит первые слова быстро, а не ждет полного ответа. Итоги использования поступают в конце потока, поэтому потоковый запрос все еще может быть точно оплачен и записан.

import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.nsfwaiapi.com/v1",
    api_key=os.environ["NSFWAIAPI_KEY"],
)

stream = client.chat.completions.create(
    model="uncensored",
    messages=[{"role": "user", "content": "Write a short scene: two rival smugglers share a lantern during a storm."}],
    stream=True,
)

text = ""
for chunk in stream:
    if chunk.choices and chunk.choices[0].delta.content:
        piece = chunk.choices[0].delta.content
        text += piece
        print(piece, end="", flush=True)
    usage = getattr(chunk, "usage", None)
    if usage:
        print("\n[usage]", usage)

Два нюанса экономят время на отладку. Во-первых, последний чанк может содержать пустой список choices, поэтому цикл проверяет его перед индексацией. Во-вторых, запрос с потоковой передачей удерживает слот параллельных запросов до завершения потока, поэтому если ваш код прерывает поток без его закрытия, этот слот остаётся занятым. Используйте with client.chat.completions.create(..., stream=True) as stream: или вызовите stream.close(), если прекращаете чтение досрочно.

JSON-режим для структурированного вывода

Когда вашей программе нужны данные, а не текст, например, лист персонажа, список сцен или теги для истории, используйте JSON-режим с response_format={"type": "json_object"}. Также укажите "JSON" в промпте и опишите ключи, которые вы хотите. Настройка запрашивает объект JSON, но не гарантирует, что ваши ключи будут присутствовать, поэтому проверяйте их.

import json
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.nsfwaiapi.com/v1",
    api_key=os.environ["NSFWAIAPI_KEY"],
)

resp = client.chat.completions.create(
    model="uncensored",
    response_format={"type": "json_object"},
    messages=[
        {"role": "system", "content": "Reply with a JSON object with keys: name, age, goal, secret. All characters are adults."},
        {"role": "user", "content": "Create a ship's cook for a pirate adventure."},
    ],
    temperature=0.7,
)

sheet = json.loads(resp.choices[0].message.content)
missing = [k for k in ("name", "age", "goal", "secret") if k not in sheet]
if missing:
    raise ValueError(f"model skipped keys: {missing}")
print(sheet["name"], "-", sheet["goal"])

Если длинный ответ обрезан max_tokens, JSON будет неполным, и json.loads завершится ошибкой. Оставьте max_tokens незаданным, так как это необязательный параметр, и генерация может использовать оставшуюся часть окна, или установите его с запасом.

Вызов функций

Вызов инструментов позволяет модели просить ваш код что-то сделать, например, бросить кубики, посмотреть заметку персонажа или получить цену. Вы передаете tools и, если хотите, tool_choice. Ответ содержит tool_calls; вы выполняете их и отправляете результаты обратно как сообщения с role: "tool". Потоковые ответы также содержат tool_calls в дельтах.

import json
import os
import random
from openai import OpenAI

client = OpenAI(
    base_url="https://api.nsfwaiapi.com/v1",
    api_key=os.environ["NSFWAIAPI_KEY"],
)

tools = [{
    "type": "function",
    "function": {
        "name": "roll_die",
        "description": "Roll one die with the given number of sides",
        "parameters": {
            "type": "object",
            "properties": {"sides": {"type": "integer", "minimum": 2}},
            "required": ["sides"],
        },
    },
}]

messages = [
    {"role": "system", "content": "You run a tabletop fiction game. Use roll_die for any chance-based outcome."},
    {"role": "user", "content": "I try to pick the lock on the captain's cabin. Roll a d20."},
]

first = client.chat.completions.create(
    model="uncensored", messages=messages, tools=tools, tool_choice="auto"
)
msg = first.choices[0].message

if msg.tool_calls:
    messages.append(msg)
    for call in msg.tool_calls:
        args = json.loads(call.function.arguments)
        result = random.randint(1, args["sides"])
        messages.append({"role": "tool", "tool_call_id": call.id, "content": str(result)})
    final = client.chat.completions.create(
        model="uncensored", messages=messages, tools=tools
    )
    print(final.choices[0].message.content)
else:
    print(msg.content)

Рассматривайте аргументы как ненадёжный ввод. Проверяйте их перед использованием, точно так же, как вы проверяете данные веб-форм. Модели иногда отправляют отсутствующее поле или значение неверного типа, и небольшая проверка вокруг json.loads предотвратит сбой вашего приложения из-за одного плохого вызова.

Лимиты запросов, повторные попытки и ваш баланс

Лимиты защищают сервис и дают вам предсказуемое поведение. Полный ключ позволяет 8 параллельных запросов и 300 запросов в минуту. Пробный ключ позволяет 2 и 60. Максимальный размер тела — 8 МБ. Два вида ошибок лимита возвращаются как HTTP 429: код rate_limited означает слишком много запросов в минуту, а concurrency — слишком много одновременно. Временная перегрузка также возвращает 429 с заголовком Retry-After: 5.

SDK от OpenAI по умолчанию повторяет запросы при нескольких ошибках. Для контроля отключите это и напишите небольшой цикл, который учитывает Retry-After:

import os
import time
from openai import OpenAI, RateLimitError

client = OpenAI(
    base_url="https://api.nsfwaiapi.com/v1",
    api_key=os.environ["NSFWAIAPI_KEY"],
    max_retries=0,
)

def ask(messages, tries=5):
    for attempt in range(tries):
        try:
            raw = client.chat.completions.with_raw_response.create(
                model="uncensored", messages=messages
            )
            print("balance:", raw.headers.get("X-Balance-USD"), "request id:", raw.headers.get("X-Request-Id"))
            return raw.parse()
        except RateLimitError as err:
            wait = float(err.response.headers.get("Retry-After", 2 ** attempt))
            time.sleep(wait)
    raise RuntimeError("still rate limited after retries")

reply = ask([{"role": "user", "content": "Describe a market at dawn in two sentences."}])
print(reply.choices[0].message.content)

Каждый ответ содержит X-Request-Id, который вы укажите, если свяжетесь с поддержкой, и X-Balance-USD, который показывает ваш оставшийся баланс. Заголовки X-RateLimit-Limit-Requests и X-RateLimit-Limit-Concurrency сообщают вам, какие лимиты применяются к вашему ключу. Чтобы запустить много задач одновременно, ограничьте параллелизм в своем коде семафором, размером с ваш ключ, 8 для полного ключа и 2 для пробного ключа, чтобы вы редко видели 429 вообще.

Выполнение множества запросов параллельно

Пакетные задания, такие как генерация тегов для нескольких сотен фрагментов истории, — это то, где важны лимиты параллелизма. Полный ключ принимает 8 запросов одновременно, поэтому выполняйте работу через асинхронный клиент и семафор такого размера. Если вы на пробном ключе, используйте 2.

import asyncio
import os
from openai import AsyncOpenAI

client = AsyncOpenAI(
    base_url="https://api.nsfwaiapi.com/v1",
    api_key=os.environ["NSFWAIAPI_KEY"],
)
gate = asyncio.Semaphore(8)

async def summarize(fragment):
    async with gate:
        resp = await client.chat.completions.create(
            model="uncensored",
            messages=[
                {"role": "system", "content": "Summarize the passage in one sentence."},
                {"role": "user", "content": fragment},
            ],
            temperature=0.3,
        )
    return resp.choices[0].message.content

async def main(fragments):
    return await asyncio.gather(*(summarize(f) for f in fragments))

results = asyncio.run(main(["The tide came in early.", "Marta hid the ledger under the floor."]))
print(results)

Семафор — это главный секрет. Без него список из 200 фрагментов запускает 200 запросов одновременно, и большинство из них возвращаются с ошибкой 429 и кодом concurrency. С ним работа выполняется со скоростью, которую позволяет ваш ключ, и ничего не тратится впустую. Если вы также устанавливаете лимит в минуту, 300 запросов в минуту на полном ключе составляют пять в секунду, поэтому очень короткие запросы могут достичь лимита в минуту раньше, чем лимита параллелизма.

Распространённые проблемы и решения

  • 401 или ошибки аутентификации. Ключ отсутствует, обрезан или отправлен на неправильный хост. Распечатайте client.base_url и проверьте переменную окружения. Заголовок должен иметь вид Authorization: Bearer, за которым следует ваш ключ.
  • Модель не найдена. Единственный идентификатор модели — uncensored. Скопированный пример с другим именем завершится ошибкой, поэтому вызовите GET /v1/models, если не уверены.
  • Пустые или короткие ответы в цикле истории. Проверьте, что вы добавляете ответ ассистента в список сообщений перед следующим ходом. Без этого модель видит только ваше последнее сообщение.
  • Повторяющиеся ответы. Немного увеличьте frequency_penalty, например, до 0,3, или уменьшите temperature, если текст уходит в сторону. Все параметры, включая top_p, stop и seed, передаются.
  • Разный вывод при каждом запуске. Установите seed и низкую temperature, когда вам нужны результаты, которые максимально повторяемы, пока вы отлаживаете.
  • Запрос слишком большой. Лимит тела запроса — 8 МБ, а промпт плюс завершение должны укладываться в 100 000 токенов. Удалите лишнее или сократите старый контекст, прежде чем он достигнет любого из лимитов.

Когда что-то выглядит неправильно на стороне сервера, сохраните заголовок X-Request-Id из ответа и отправьте его на страницу поддержки вместе с кратким описанием. Это позволяет операторам найти ваш точный запрос.

Стоимость и ограничения

Цены составляют $0,15 за 1M входных токенов и $0,50 за 1M выходных токенов, из предоплаченного баланса без подписки. Неудачные или отклоненные запросы не оплачиваются, и баланс не сгорает. В качестве примера, скрипт, который отправляет 3 000 токенов и получает 800, платит $0,00045 за вход плюс $0,0004 за выход, что составляет $0,00085 за вызов, поэтому 1 000 вызовов стоят $0,85. Пополнения — от $20 до $500, с бонусами +5% от $50, +10% от $100 и +15% от $200. Этот сайт принимает только криптовалюту, USDT на TRC20 или USDC на Base. См. страницу цен для полной таблицы и документацию для справки по эндпоинтам.

Границы ясны. Это единственная модель с открытыми весами, запущенная независимым провайдером, и это не официальный API какого-либо вендора моделей. Размер контекстного окна составляет 100 000 токенов для промпта плюс завершение. Она не генерирует изображения, аудио или видео, не предоставляет эмбеддинги и не поддерживает дообучение. Используйте её только для законного использования взрослыми и обратите внимание на жесткое ограничение: запрет на сексуальный контент с участием несовершеннолетних. Промпты не используются для обучения.

Вопросы и ответы

Нужен ли специальный SDK для вызова этого API из Python?

Нет. Работает официальный пакет openai. Установите base_url в https://api.nsfwaiapi.com/v1, передайте ключ и используйте идентификатор модели uncensored.

Работает ли потоковая передача так же, как у OpenAI?

Да. Передайте stream=True и читайте события, отправляемые сервером, по частям. Использование указывается в конце потока.

Что делать при получении ошибки 429?

Подождите и повторите попытку. Если в ответе есть заголовок Retry-After, учтите его. Код ошибки rate_limited означает слишком много запросов в минуту, а concurrency — слишком много запросов одновременно.

Могу ли я протестировать это без оплаты?

Да. После регистрации вы можете получить $0,50 пробного баланса на странице API-ключа. Для этого не нужны данные карты, срок действия — 7 дней, и он предоставляется один раз на человека.

До вашего ключа — одна форма

Создайте аккаунт, скопируйте ключ, измените базовый URL. Это вся настройка.

Получить API-ключ

NsfwAiApiAPI LLM без цензуры для генерации NSFW-текста