An async, fully typed Python client for the NovelAI API, covering image generation (with first-class support for the V4.5 model family), text generation, Director image tools, and account utilities.
Request payloads are validated with pydantic and verified field-by-field against payloads captured from the NovelAI web client, so what this library sends is what the website sends.
- Image generation β text-to-image, img2img, inpainting, and vibe transfer
(reference images are encoded through
/ai/encode-vibeautomatically, with caching) for every model from V3 to V4.5. - V4.5 support β multi-character prompts with canvas positioning, character-level undesired content, and real-time streaming of denoising steps.
- Text generation β story continuation with Erato, Kayra, and Clio. Plain text in, plain text out, with optional token-by-token streaming.
- Director tools β line art, sketch, background removal, declutter, colorize, and emotion change, each as a single method call.
- Utilities β upscaling, tag autocompletion, ControlNet annotation, and subscription/Anlas queries.
- Robust by default β automatic retry with backoff on rate limits, optional client-side request throttling, and typed exceptions.
- CLI β the
nekoaicommand exposes generation, tools, and account queries for shell usage and scripting. - Custom hosts β every endpoint group can be pointed at a reverse proxy or self-hosted gateway.
Requires Python 3.10 or later.
pip install -U nekoai-api
# or
uv add nekoai-apiimport asyncio
from nekoai import Model, NovelAI, Resolution
async def main():
# Reads the token from the NAI_TOKEN environment variable when omitted
async with NovelAI(token="your_access_token") as client:
images = await client.generate_image(
prompt="1girl, silver hair, blue eyes, white dress, flower garden",
model=Model.V4_5,
res_preset=Resolution.NORMAL_PORTRAIT,
)
for image in images:
image.save("output")
asyncio.run(main())The client accepts either an access token or account credentials:
NovelAI(token="...") # explicit token
NovelAI() # token from $NAI_TOKEN
NovelAI(username="user@example.com", password="...")To obtain a persistent token, log in once and store the result:
nekoai login <username> <password>generate_image accepts either a prepared Metadata object or keyword
arguments, and returns a list of Image objects. All parameters are validated
before the request is sent; see the Metadata docstring for the full set.
| Model | Enum | Inpainting variant |
|---|---|---|
| NAI Diffusion V4.5 Full (recommended) | Model.V4_5 |
Model.V4_5_INP |
| NAI Diffusion V4.5 Curated | Model.V4_5_CUR |
Model.V4_5_CUR_INP |
| NAI Diffusion V4 Full | Model.V4 |
Model.V4_INP |
| NAI Diffusion V4 Curated | Model.V4_CUR |
Model.V4_CUR_INP |
| NAI Diffusion V3 | Model.V3 |
Model.V3_INP |
| NAI Diffusion Furry V3 | Model.FURRY |
Model.FURRY_INP |
images = await client.generate_image(
prompt="1girl, cute",
model=Model.V4_5, # default: Model.V4_5
res_preset=Resolution.NORMAL_SQUARE, # or width=..., height=...
steps=28,
scale=6.0, # prompt guidance
seed=1234567890, # random when omitted
n_samples=1,
negative_prompt="lowres",
qualityToggle=True, # append model quality tags
ucPreset=0, # undesired-content preset (varies by model)
)Each character gets its own prompt, undesired content, and canvas position:
from nekoai import CharacterPrompt, PositionCoords
images = await client.generate_image(
prompt="two people standing together, park background",
model=Model.V4_5,
res_preset=Resolution.NORMAL_LANDSCAPE,
characterPrompts=[
CharacterPrompt(
prompt="girl, red hair, red dress",
uc="bad hands, bad anatomy",
center=PositionCoords(x=0.3, y=0.5),
),
CharacterPrompt(
prompt="boy, blue hair, blue uniform",
uc="bad hands, bad anatomy",
center=PositionCoords(x=0.7, y=0.5),
),
],
)generate_image_stream yields events for every denoising step, which is useful
for progress UIs and timelapses:
from nekoai import EventType
async for event in client.generate_image_stream(
prompt="1girl, cute, anime style",
model=Model.V4_5,
res_preset=Resolution.NORMAL_PORTRAIT,
):
if event.event_type == EventType.INTERMEDIATE:
print(f"step {event.step_ix} (sigma={event.sigma:.2f})")
elif event.event_type == EventType.FINAL:
event.image.save("output", "final.png")generate_image works with every model and returns the final images;
streaming requires a V4/V4.5 model.
from nekoai import Action, parse_image
width, height, base64_image = parse_image("input/source.png")
images = await client.generate_image(
prompt="1girl, fantasy outfit",
model=Model.V4_5,
action=Action.IMG2IMG,
width=width,
height=height,
image=base64_image,
strength=0.5, # lower = closer to the original
noise=0.1,
)Provide a base image and a black/white mask (white areas are repainted) and use an inpainting model:
from nekoai import Action, parse_image
width, height, base64_image = parse_image("input/portrait.png")
_, _, base64_mask = parse_image("input/mask.png")
images = await client.generate_image(
prompt="1girl, detailed background",
model=Model.V4_5_INP,
action=Action.INPAINT,
width=width,
height=height,
image=base64_image,
mask=base64_mask,
add_original_image=True, # overlay untouched pixels from the original
)Borrow the style and mood of reference images. For V4/V4.5 models the client
encodes references through /ai/encode-vibe automatically (2 Anlas per new
image; results are cached for the client's lifetime, and the Metadata object
can be reused across calls):
from nekoai import parse_image
_, _, reference = parse_image("input/style_reference.png")
images = await client.generate_image(
prompt="landscape, mountains, sunset",
model=Model.V4_5,
res_preset=Resolution.NORMAL_LANDSCAPE,
reference_image_multiple=[reference],
reference_information_extracted_multiple=[1.0],
reference_strength_multiple=[0.7],
)Continue a story with NovelAI's text models. Input and output are plain text β no tokenizer required. Declared parameters are validated, and any additional sampling parameter accepted by the API (mirostat, cfg, phrase repetition penalty, ...) passes through unchanged:
from nekoai import TextModel
output = await client.generate_text(
"The dragon circled the tower once more,",
model=TextModel.ERATO, # llama-3-erato-v1 (default)
max_length=150,
temperature=1.0,
)
# Stream tokens as they are generated:
async for token in client.generate_text_stream("Once upon a time,", max_length=50):
print(token, end="", flush=True)| Model | Enum | Identifier |
|---|---|---|
| Erato (recommended) | TextModel.ERATO |
llama-3-erato-v1 |
| Kayra | TextModel.KAYRA |
kayra-v1 |
| Clio | TextModel.CLIO |
clio-v1 |
Other model identifiers can be passed as plain strings. Note that
generate_until_sentence (enabled by default) lets the model run slightly past
max_length to finish a sentence; disable it for hard caps.
Every Director tool is a single method call. Image inputs accept a file path,
pathlib.Path, raw bytes, a file-like object, or a base64 string.
from nekoai import EmotionLevel, EmotionOptions
result = await client.lineart("image.png") # image to line art
result = await client.sketch("image.png") # image to sketch
result = await client.background_removal("image.png") # remove background (costs Anlas)
result = await client.declutter("image.png") # remove text/artifacts
result = await client.colorize("lineart.png", prompt="silver hair", defry=0)
result = await client.change_emotion(
"image.png",
emotion=EmotionOptions.HAPPY,
emotion_level=EmotionLevel.NORMAL,
)
result.save("output")# Upscale 2x or 4x (costs Anlas)
upscaled = await client.upscale("image.png", scale=4)
# Tag autocompletion
tags = await client.suggest_tags("blue hai") # [{"tag": "blue hair", ...}, ...]
# ControlNet condition masks (edge/depth preprocessing).
# Note: ControlNet-guided generation is a V1/V2-era feature not supported by V3+.
from nekoai import Controlnet
mask = await client.annotate_image("image.png", model=Controlnet.SCRIBBLER)
# Subscription tier and Anlas balance
subscription = await client.get_subscription()
user_data = await client.get_user_data()Authentication comes from --token, the NAI_TOKEN environment variable, or
--username/--password.
| Command | Purpose |
|---|---|
nekoai login <user> <pass> |
Exchange credentials for an access token |
nekoai generate <prompt> |
Generate images (txt2img, img2img, inpaint, vibe) |
nekoai text <prompt> |
Generate a text continuation |
nekoai tool <name> <image> |
Run a Director or annotation tool |
nekoai upscale <image> |
Upscale an image 2x or 4x |
nekoai tags <partial> |
Suggest completions for a partial tag |
nekoai subscription |
Show subscription info and Anlas balance |
export NAI_TOKEN="your_access_token"
nekoai generate "1girl, cute" -m v4_5 -s 832x1216 --steps 28 -n 2
nekoai generate "1girl, cute" --stream # live step progress
nekoai generate "1girl, fantasy outfit" --image src.png --strength 0.5 # img2img
nekoai generate "detailed background" --image base.png --mask mask.png # inpaint
nekoai generate "landscape, sunset" --reference-image style.png # vibe
nekoai text "Once upon a time," --max-length 80 --stream
nekoai tool lineart image.png
nekoai tool emotion image.png --emotion happy
nekoai upscale image.png --scale 4Every host can point at a custom base URL (reverse proxy, self-hosted gateway).
host serves image and account endpoints, text_host serves text generation,
and api_host covers the endpoints still served only by the legacy API host
(upscale, ControlNet annotation):
client = NovelAI(
token="...",
host="https://your-image-proxy.example.com", # default: https://image.novelai.net
text_host="https://your-text-proxy.example.com", # default: https://text.novelai.net
api_host="https://your-api-proxy.example.com", # default: https://api.novelai.net
)Requests that hit the rate limit (HTTP 429) are retried with exponential
backoff (max_retries, default 2). For batch workloads, rate_limit enforces
a minimum spacing between requests client-side:
client = NovelAI(token="...", rate_limit=10) # at least 10 seconds between requestsclient = NovelAI(token="...")
await client.init(timeout=60) # per-request timeout in seconds
await client.init(auto_close=True, close_delay=300) # close idle connectionsAll exceptions derive from nekoai.NovelAIError:
| Exception | Raised on |
|---|---|
AuthError |
Invalid or expired credentials (HTTP 401) |
APIError |
Request validation failure (HTTP 400) |
NotEnoughCreditsError |
Insufficient Anlas or no active subscription (HTTP 402) |
ConcurrentError |
Rate limit exceeded after retries (HTTP 429) |
TimeoutError |
Request exceeded the client timeout |
ImageProcessingError |
Unreadable or unsupported input image |
from nekoai import NovelAI, NovelAIError
try:
images = await client.generate_image(prompt="1girl")
except NovelAIError as e:
print(f"generation failed: {e}")One runnable script per feature lives in examples/requests/,
each reading NAI_TOKEN from the environment. See its
README for the full index and per-feature Anlas
costs. NovelAI allows one concurrent generation per account, so run examples
one at a time.
The project uses uv for dependency management and ruff for linting and formatting:
uv sync # install dependencies (including the dev group)
uv run pytest # run the test suite
uv run ruff check . # lint
uv run ruff format . # formatPayload-shape tests compare generated request bodies against real payloads
captured from the NovelAI web client (examples/payloads/); please keep them
green when touching Metadata serialization.
Licensed under AGPL-3.0. Originally inspired by HanaokaYuzu/NovelAI-API and adopts a copyleft license accordingly.