A lightweight JavaScript/TypeScript client for the NovelAI API: image generation, Director tools, and text generation.
NekoAI-JS wraps NovelAI's image and text generation APIs behind a small, strongly typed interface. It runs in both Node.js and browsers, ships CJS and ESM builds with full TypeScript definitions, and is based on the NekoAI-API Python package.
Capabilities
- Image generation with V3, V4, and V4.5 models, including multi-character prompts, img2img, and inpainting
- Real-time streaming of V4/V4.5 generation steps
- Vibe transfer and character reference (director reference) for V4.5
- Dedicated 2x/4x upscaling and img2img-based enhancement
- All Director tools: line art, sketch, background removal, declutter, colorize, emotion change
- Text generation (chat and completions) through NovelAI's OpenAI-compatible endpoints, with streaming
- Tag autocomplete suggestions
- Metadata extraction from AI-generated images (PNG text chunks, EXIF, NovelAI stealth LSB)
- Automatic retries with exponential backoff, structured API errors, flexible image inputs
- Node.js 18 or later, or a modern browser
- A NovelAI account with an active subscription and a persistent API token
npm install nekoai-js
# or: yarn add nekoai-js / pnpm add nekoai-js / bun add nekoai-jsIn Node.js, install the optional canvas package if you plan to pass images by file path or buffer (img2img, inpainting, director tools, metadata extraction):
npm install canvasBrowsers use the native Canvas API and do not need this dependency.
import { NovelAI, Model, Resolution } from "nekoai-js";
const client = new NovelAI({ token: process.env.NOVELAI_TOKEN });
const images = await client.generateImage({
prompt: "1girl, cute, anime style, detailed",
model: Model.V4_5,
resPreset: Resolution.NORMAL_PORTRAIT,
});
await images[0].save("./output"); // Node.js
// or, in the browser:
// imgElement.src = images[0].toDataURL();The package is dual-published: import { NovelAI } from "nekoai-js" (ESM) and const { NovelAI } = require("nekoai-js") (CJS) both work.
const client = new NovelAI({
token: "your_access_token", // required
host: Host.WEB, // image API host (default: https://image.novelai.net)
textHost: Host.TEXT, // text API host (default: https://text.novelai.net)
timeout: 120000, // ms until the API responds, including generation time
retry: {
enabled: true, // default: true
maxRetries: 3, // default: 3
baseDelay: 1000, // exponential backoff base, ms
maxDelay: 30000, // backoff cap, ms
retryStatusCodes: [429, 500, 502, 503, 504],
},
verbose: false, // log payloads and estimated Anlas cost
});generateImage(metadata) accepts a Metadata object. Every field is optional except prompt in practice; unset fields receive the same defaults the NovelAI web UI uses (model V4_5, 28 steps, scale 6.0, Euler Ancestral sampler, quality tags and negative-prompt preset applied).
const images = await client.generateImage({
prompt: "1girl, cute, anime style",
model: Model.V4_5,
resPreset: Resolution.NORMAL_PORTRAIT,
n_samples: 1,
steps: 28,
scale: 6.0,
seed: 1234567890, // omit for a random seed
qualityToggle: true, // append model-specific quality tags
ucPreset: 0, // negative-prompt preset strength (0-3, model dependent)
});| Enum | API id |
|---|---|
Model.V4_5 |
nai-diffusion-4-5-full |
Model.V4_5_INP |
nai-diffusion-4-5-full-inpainting |
Model.V4_5_CUR |
nai-diffusion-4-5-curated |
Model.V4_5_CUR_INP |
nai-diffusion-4-5-curated-inpainting |
Model.V4 |
nai-diffusion-4-full |
Model.V4_INP |
nai-diffusion-4-full-inpainting |
Model.V4_CUR |
nai-diffusion-4-curated-preview |
Model.V4_CUR_INP |
nai-diffusion-4-curated-inpainting |
Model.V3 |
nai-diffusion-3 |
Model.V3_INP |
nai-diffusion-3-inpainting |
Model.FURRY |
nai-diffusion-furry-3 |
Model.FURRY_INP |
nai-diffusion-furry-3-inpainting |
resPreset sets width/height unless you specify them explicitly. Explicit dimensions are rounded up to multiples of 64 and validated against the API's pixel budget.
| Preset | Dimensions |
|---|---|
SMALL_PORTRAIT / SMALL_LANDSCAPE / SMALL_SQUARE |
512x768 / 768x512 / 640x640 |
NORMAL_PORTRAIT / NORMAL_LANDSCAPE / NORMAL_SQUARE |
832x1216 / 1216x832 / 1024x1024 |
LARGE_PORTRAIT / LARGE_LANDSCAPE / LARGE_SQUARE |
1024x1536 / 1536x1024 / 1472x1472 |
WALLPAPER_PORTRAIT / WALLPAPER_LANDSCAPE |
1088x1920 / 1920x1088 |
V4/V4.5 generations can stream each denoising step. Pass true as the second argument; the return type narrows to an async generator of MsgpackEvent objects.
import { EventType } from "nekoai-js";
const stream = await client.generateImage(
{ prompt: "1girl, night sky", model: Model.V4_5 },
true,
);
for await (const event of stream) {
if (event.event_type === EventType.INTERMEDIATE) {
console.log(`step ${event.step_ix}`); // event.image is a JPEG preview
} else if (event.event_type === EventType.FINAL) {
await event.image.save("./output/final.png");
}
}Streaming with a V3 model throws, since V3 only returns final images.
Every image-bearing field (image, mask, reference_image_multiple, director_reference_images, and all director tool / upscale / enhance arguments) accepts any of:
- file path (Node.js)
- HTTP(S) URL, data URL, or blob URL
- raw base64 string
Blob,File,ArrayBuffer,Uint8ArrayHTMLImageElement,HTMLCanvasElement(browser){ data: Uint8Array }(for example, a generatedImageobject)
Conversion happens automatically; you no longer need to call parseImage yourself (it remains exported for cases where you need dimensions).
import { Action } from "nekoai-js";
const images = await client.generateImage({
prompt: "1girl, fantasy outfit",
action: Action.IMG2IMG,
image: "./input/image.png",
strength: 0.5, // lower = closer to the source
noise: 0.1,
});Use an inpainting model with Action.INPAINT. White mask areas are regenerated.
const images = await client.generateImage({
prompt: "1girl, red eyes",
model: Model.V4_5_INP,
action: Action.INPAINT,
image: "./input/image.png",
mask: "./input/mask.png",
add_original_image: true, // preserve unmasked areas exactly
});const images = await client.generateImage({
prompt: "two people standing together, park background",
model: Model.V4_5,
characterPrompts: [
{
prompt: "girl, red hair, red dress",
uc: "bad hands, bad anatomy",
center: { x: 0.3, y: 0.5 }, // optional; coordinates in 0-1
},
{
prompt: "boy, blue hair, blue uniform",
center: { x: 0.7, y: 0.5 },
},
],
});Character coordinates are only sent when at least one character has a non-default center. The V4 prompt structures (v4_prompt, v4_negative_prompt) are built automatically.
Reference images are encoded into vibe tokens through /ai/encode-vibe (results are cached in-memory per client, keyed by image hash, extraction level, and model).
const images = await client.generateImage({
prompt: "1girl, cute",
model: Model.V4_5,
reference_image_multiple: ["./input/reference.png"],
reference_information_extracted_multiple: [0.7], // 0-1, default 1.0
reference_strength_multiple: [0.6], // 0-1, default 0.6
});Director reference conditions the generation on a reference character and/or style. Reference images should be 1024x1536, 1536x1024, or 1472x1472, padded with black to fit.
const images = await client.generateImage({
prompt: "1girl, dancing in the rain",
model: Model.V4_5,
director_reference_images: ["./reference/character.png"],
director_reference_descriptions: [
{ caption: { base_caption: "character&style", char_captions: [] } },
],
director_reference_information_extracted: [1],
director_reference_strength_values: [1],
director_reference_secondary_strength_values: [1], // fidelity, 0-1
});Use base_caption: "character" to transfer only the character, or "character&style" to transfer the art style as well.
// Dedicated upscaler (api.novelai.net); pixels preserved, no re-generation
const upscaled = await client.upscale("./output/image.png", 2); // scale: 2 or 4
await upscaled.save("./output/upscaled.png");
// Enhance: img2img re-generation at a scaled-up resolution
const enhanced = await client.enhance("./output/image.png", {
prompt: "1girl, cute, watercolor", // ideally the image's original prompt
upscaleFactor: 1.5, // clamped to the API's pixel budget
strength: 0.4, // lower = closer to the source
});
await enhanced[0].save("./output/enhanced.png");Each tool takes any supported image input and returns a single Image.
const lineArt = await client.lineArt("./input/image.png");
const sketch = await client.sketch("./input/image.png");
const noBackground = await client.backgroundRemoval("./input/image.png");
const decluttered = await client.declutter("./input/image.png");
const colorized = await client.colorize("./input/lineart.png", "blue hair", 0);
import { EmotionOptions, EmotionLevel } from "nekoai-js";
const happy = await client.changeEmotion(
"./input/image.png",
EmotionOptions.HAPPY,
"", // additional prompt
EmotionLevel.NORMAL, // strength of the change
);const suggestions = await client.suggestTags("blue hai");
// [{ tag: "blue hair", confidence: ..., count: ... }, ...]An optional second argument selects the model (Model.V4_5 by default) and a third the query language ("en" or "jp").
Text generation uses NovelAI's OpenAI-compatible endpoints on text.novelai.net. Query the live model list with listTextModels(); the TextModel enum covers the currently available ids (glm-4-6, xialong-v1).
import { TextModel } from "nekoai-js";
const models = await client.listTextModels();
// Chat completion; a plain string becomes a single user message
const completion = await client.chat("Describe a cozy tavern in one sentence.", {
model: TextModel.GLM_4_6, // default
max_tokens: 100,
temperature: 1.0,
});
console.log(completion.choices[0].message.content);
// Message arrays with roles
const reply = await client.chat(
[
{ role: "system", content: "You are a concise storyteller." },
{ role: "user", content: "Continue: The dragon opened one eye and" },
],
{ max_tokens: 100 },
);
// Streaming
const stream = await client.chatStream("Tell me a short story.", { max_tokens: 200 });
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta?.content;
if (delta) process.stdout.write(delta);
}
// Raw text completion
const continuation = await client.completion("The old lighthouse keeper", {
max_tokens: 100,
});
console.log(continuation.choices[0].text);Options follow the OpenAI parameter names (max_tokens, temperature, top_p, top_k, min_p, frequency_penalty, presence_penalty, stop, seed, logit_bias, n); unknown keys are passed through to the API unchanged.
Generation methods return Image objects:
| Member | Description |
|---|---|
image.data |
Raw bytes (Uint8Array) |
image.filename |
Timestamped default filename |
image.size |
Size in bytes |
await image.save(path) |
Write to disk (Node.js). Directories are created as needed; a trailing / or extension-less path is treated as a directory |
image.toBase64() |
Base64 string |
image.toDataURL() |
data: URL for direct use in img.src |
image.toBlob() / image.toFile() |
Browser Blob / File |
Read generation parameters back out of AI-generated images. Supports NovelAI PNG text chunks, NovelAI stealth metadata (alpha-channel LSB), and Stable Diffusion WebUI EXIF/parameters formats.
import { extractImageMetadata, getImageSummary } from "nekoai-js";
const metadata = await extractImageMetadata("./image.png");
// { type: "NOVELAI" | "SD-WEBUI" | "NONE", entries: [{ keyword, text }, ...] }
const summary = await getImageSummary("./image.png");
// { dimensions, generationTool, positivePrompt, parameters, ... }API failures throw NovelAIApiError, which carries the HTTP status and the error message returned by the API:
import { NovelAIApiError } from "nekoai-js";
try {
await client.generateImage({ prompt: "1girl" });
} catch (err) {
if (err instanceof NovelAIApiError) {
console.error(err.status, err.message); // e.g. 402 "Not enough Anlas"
}
}Retryable failures (rate limits, 5xx responses, network errors, timeouts) are retried automatically with exponential backoff and jitter according to the client's retry configuration. Set retry: { enabled: false } to disable.
| Method | Description |
|---|---|
generateImage(metadata, stream?, isOpus?) |
Generate images; stream: true returns an async generator of step events |
enhance(image, options?) |
img2img re-generation at a scaled-up resolution |
upscale(image, scale?) |
Dedicated 2x/4x upscaler |
lineArt(image) / sketch(image) / backgroundRemoval(image) / declutter(image) |
Director tools |
colorize(image, prompt?, defry?) |
Colorize sketch or line art |
changeEmotion(image, emotion?, prompt?, level?) |
Change a character's emotion |
suggestTags(prompt, model?, lang?) |
Tag autocomplete |
chat(messages, options?) |
Chat completion (OpenAI format) |
chatStream(messages, options?) |
Streaming chat completion |
completion(prompt, options?) |
Raw text completion |
listTextModels() |
Available text model ids |
useDirectorTool(request) |
Low-level director tool access |
The library works in browsers without extra dependencies; image parsing uses the DOM Canvas API. Note that calling the NovelAI API directly from a browser is subject to CORS policy, and embedding a user's token in client-side code should be handled with care. For user-facing applications, NovelAI recommends asking each user for their own persistent API token.
<script type="module">
import { NovelAI, Model } from "./node_modules/nekoai-js/dist/index.mjs";
const client = new NovelAI({ token });
const images = await client.generateImage({ prompt: "1girl", model: Model.V4_5 });
document.querySelector("img").src = images[0].toDataURL();
</script>npm install
npm run typecheck # tsc --noEmit
npm test # vitest (offline unit tests)
npm run lint # eslint
npm run build # tsup -> dist/ (CJS + ESM + d.ts)Examples in examples/ run against the live API and expect a NOVELAI_TOKEN entry in .env.
Licensed under AGPL-3.0.
This project transitioned from MIT to AGPL-3.0 to align with its inspiration source, NekoAI-API, and to provide stronger copyleft protections for the community.