A lightweight multimodal generation SDK with built-in model presets, model declaration files, and adapter-based provider calls.
npm install @neta-art/generationimport { createGenerationClient } from "@neta-art/generation";
const client = createGenerationClient({
apiKey: process.env.NETA_ROUTER_API_KEY!,
});
const output = await client.generate({
model: "gpt-image-2",
content: [
{ type: "text", text: "a cinematic portrait of a robot florist, 35mm film" },
],
parameters: {
size: "1024x1024",
quality: "high",
},
});
console.log(output);baseUrl defaults to https://router.neta.art. Pass a different endpoint when needed:
const client = createGenerationClient({
apiKey: process.env.NETA_ROUTER_API_KEY!,
baseUrl: "https://router.neta.art",
});.env is ignored by Git. Copy .env.example to .env and fill in your router key:
cp .env.example .envNETA_ROUTER_API_KEY=your_api_key_hereNode.js does not load .env automatically for library code. The example scripts use Node's native --env-file flag through npm scripts:
pnpm example:basic-image
pnpm example:image-editing
pnpm example:text-to-videoLive provider tests are separate from pnpm test because they use the real SDK client and submit real provider requests. Set
NETA_ROUTER_API_KEY or NETA_API_KEY, then run:
pnpm test:live:sunoYou can also call providers through the CLI:
node --env-file=.env ./dist/cli/index.js generate gemini-3.1-flash-image-preview \
--prompt "a simple abstract geometric app icon" \
--param aspect_ratio=1:1 \
--param image_size=512 \
--debugUse --image-url for reference images, --out to write base64 outputs to files, and json: for non-string parameter values, for example --param duration=json:5.
Pass debug: true to print the final provider request and response metadata to stderr. Sensitive fields such as Authorization and base64 image data are redacted by default.
const client = createGenerationClient({
apiKey: process.env.NETA_ROUTER_API_KEY!,
debug: true,
});For a custom logger or unredacted secret headers. Base64 media payloads are always redacted from debug events:
const client = createGenerationClient({
apiKey: process.env.NETA_ROUTER_API_KEY!,
debug: {
enabled: true,
includeSensitive: true,
logger: (event) => console.error(JSON.stringify(event, null, 2)),
},
});gpt-image-2z-image-turboqwen-image-editgemini-3.1-flash-image-previewkling-text-to-videokling-image-to-videokling-omni-videokling-multi-image-to-videoseedance-2-0seedance-2-0-fastsuno_music_chirp_fenixnoobxl-t2i-onediffnoobxl-i2i-ipa-onediffbirefnet-generalsuno_style_tagssuno_upload_audiosuno_cover_chirp_v5suno_infill_chirp_v5suno_sound_chirp_v5suno_image_to_song_chirp_v5suno_video_to_song_chirp_v5suno_vox_chirp_v5
Built-in model declarations share the same client-level apiKey and baseUrl.
const output = await client.generate({
model: "gemini-3.1-flash-image-preview",
content: [
{ type: "text", text: "turn this portrait into a watercolor illustration" },
{ type: "image", source: { type: "url", url: "https://example.com/portrait.jpg" } },
],
parameters: {
aspect_ratio: "3:4",
image_size: "2K",
},
});These image models use the same client API as the other built-in models:
z-image-turboqwen-image-editnoobxl-t2i-onediffnoobxl-i2i-ipa-onediffbirefnet-general
await client.generate({
model: "z-image-turbo",
content: [{ type: "text", text: "a clean product-style image of a small red toy robot" }],
parameters: {
size: "1024*1024",
},
});
await client.generate({
model: "qwen-image-edit",
content: [
{ type: "text", text: "change the background to a clean white studio backdrop" },
{ type: "image", source: { type: "url", url: "https://example.com/input.png" } },
],
parameters: {
size: "1024x1024",
},
});
await client.generate({
model: "noobxl-t2i-onediff",
content: [{ type: "text", text: "anime key visual, luminous city at night" }],
parameters: {
size: "1024x1024",
negative_prompt: "low quality, blurry",
},
});
await client.generate({
model: "birefnet-general",
content: [
{ type: "image", source: { type: "url", url: "https://example.com/portrait.png" } },
],
});const output = await client.generate({
model: "seedance-2-0-fast",
content: [
{ type: "text", text: "a cat playing piano in a cozy jazz club, cinematic lighting" },
],
parameters: {
duration: 5,
resolution: "720p",
aspect_ratio: "16:9",
},
});Frame and reference-image video modes use meta.role:
await client.generate({
model: "seedance-2-0",
content: [
{ type: "text", text: "create a smooth dramatic transition" },
{ type: "image", source: { type: "url", url: "https://example.com/start.jpg" }, meta: { role: "first_frame" } },
{ type: "image", source: { type: "url", url: "https://example.com/end.jpg" }, meta: { role: "last_frame" } },
],
});Kling exposes stable capability model ids while the adapter sends the latest upstream model_name for each capability:
await client.generate({
model: "kling-image-to-video",
content: [
{ type: "text", text: "gently turn toward the camera with soft natural motion" },
{ type: "image", source: { type: "url", url: "https://example.com/input.png" } },
],
parameters: {
duration: 5,
aspect_ratio: "16:9",
},
});const output = await client.generate({
model: "suno_music_chirp_fenix",
content: [
{ type: "text", text: "uplifting cinematic pop with warm piano and clear chorus" },
],
meta: {
title: "Warm Horizon",
tags: "cinematic pop, warm piano",
make_instrumental: false,
},
});
console.log(output);Suno uses one shared adapter with a small public model set: suno_music_chirp_fenix, suno_style_tags,
suno_upload_audio, suno_cover_chirp_v5, suno_infill_chirp_v5, suno_sound_chirp_v5,
suno_image_to_song_chirp_v5, suno_video_to_song_chirp_v5, and suno_vox_chirp_v5. Provider-specific fields such as
title, tags, make_instrumental, and metadata_params belong in meta.
suno_music is removed in this release. Migrate to a concrete model name and stop sending parameters.operation or meta.task.
import { createGenerationClientFromDirectory } from "@neta-art/generation";
const client = await createGenerationClientFromDirectory("./models", {
apiKey: process.env.NETA_ROUTER_API_KEY!,
});Supported declaration formats:
.yaml.yml.json
Custom declarations are merged with built-in models by default. If the same model exists, the custom declaration wins.
import { exportBuiltinModelConfig } from "@neta-art/generation";
await exportBuiltinModelConfig("gpt-image-2", "./gpt-image-2.yaml");CLI:
neta-generation models list
neta-generation models export gpt-image-2 --out ./gpt-image-2.yaml
neta-generation models export-all --out ./modelsschema: neta.generation.model.v1
model: gpt-image-2
title: GPT Image 2
adapter:
type: openai.images
content:
input:
- type: text
required: true
min: 1
max: 16
merge: newline
- type: image
required: false
max: 16
sources:
- url
- base64
parameters:
size:
type: string
optional: true
default: 1024x1024Adapter credentials are intentionally not stored in model declarations. Use client-level or request-level apiKey and baseUrl instead.
type GenerationSource =
| { type: "url"; url: string }
| { type: "base64"; mediaType: string; data: string };Built-in adapters:
openai.imagesopenai.imageEditsgemini.generateContentark.videoGenerationskling.videoGenerationssuno.tasks
You can register custom adapters:
const client = createGenerationClient({
apiKey,
adapters: {
"custom.adapter": async (input) => {
return [];
},
},
});const resolved = client.validate({
model: "gpt-image-2",
content: [{ type: "text", text: "hello" }],
});
console.log(resolved.parameters);import { GenerationValidationError, GenerationProviderError } from "@neta-art/generation";
try {
await client.generate(request);
} catch (error) {
if (error instanceof GenerationValidationError) {
console.error("Invalid request", error.message);
} else if (error instanceof GenerationProviderError) {
console.error("Provider failed", error.message);
}
}