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);Use generateResult when you need observed request metadata:
const result = await client.generateResult({
model: "gpt-image-2",
content: [{ type: "text", text: "a small red toy robot on a white desk" }],
});
console.log(result.content);
console.log(result.requestId, result.cost);requestId uses the response body's top-level request_id, then falls back to x-request-id and x-oneapi-request-id response headers. cost maps to the official request price in usage.cost when provided.
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",
});Agents and external tools should inspect a model declaration before constructing a request. Declarations expose the accepted content blocks, source types, parameters, meta fields, descriptions, and validated request examples.
import { createGenerationClient } from "@neta-art/generation";
const discoveryClient = createGenerationClient();
const declaration = discoveryClient.getModel("qwen-tts");
if (!declaration) throw new Error("Model is unavailable");
console.log(discoveryClient.stringifyModelConfig(declaration.model, { format: "json" }));
const request = declaration.examples?.find((example) => example.title === "Voice design")?.request;
if (!request) throw new Error("Voice-design example is unavailable");
// Discovery and validation do not require an API key or access the network.
discoveryClient.validate(request);
const client = createGenerationClient({ apiKey: process.env.NETA_ROUTER_API_KEY! });
const result = await client.generateResult(request);
console.log(result.content, result.requestId);Use listModels() to discover all models and getModel() for one machine-readable declaration. Treat examples as request templates, then call validate() after applying user input because adapter validation enforces cross-field rules that a structural declaration cannot fully express. Use generateResult() when the Agent needs request IDs or cost metadata.
The same declarations can be exported as YAML through the existing CLI:
neta-generation models list
neta-generation models export qwen-tts --out ./qwen-tts.yaml
neta-generation models export-all --out ./models.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-speech
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:suno
pnpm test:live:audio-speechSeedance live smoke tests exercise text-to-video, first/last frame video, and multi-reference-image plus reference-video requests through the built SDK:
pnpm test:live:seedanceThe script reads NETA_ROUTER_API_KEY or NETA_API_KEY, falling back to /tmp/neta-router-key, and writes reusable
JSON results under /tmp/neta-generation-live/seedance. Use -- --download to also save generated media files.
It also writes visual-review.html, which places the inputs and outputs side by side so the result can be checked for
actual first/last-frame or reference-media effect. Rebuild that report from an existing run with -- --report-only.
You 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. Media URLs remain complete, including query strings and fragments, so treat debug output as sensitive diagnostic data:
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-editqwen-ttsqwen-audio-3.0-tts-plusqwen-audio-3.0-tts-flashhiggs-ttsgemini-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" } },
],
});Each TTS request accepts exactly one non-empty text block and returns one URL audio block. Choose the model from the requested voice mode and priority before constructing the request:
| Requirement | Model choice |
|---|---|
| Create a voice from a text-only description, without reference audio | Use an explicitly requested Qwen variant; otherwise use qwen-tts as the deterministic default |
| Maximize fidelity to one reference voice | higgs-tts |
| Blend 2-16 weighted reference voices | higgs-tts |
| Use a default voice, including a delegated choice expressed only as any, random, suitable, or natural | higgs-tts |
- Qwen:
voice_promptdesign OR one-reference clone;qwen-ttsis the unspecified-design default and accepts any text length; Plus / Flash require at least 15 Unicode code points. - Higgs: delegated default voice, high-fidelity one-reference clone, or weighted 2-16-reference blend.
- Conflict: reference + redesign requires user choice before generation.
- Blend: all references, full text, one request.
- Dependency: clone prior generated audio.
- Ranking: no declared Qwen quality, latency, or cost order.
await client.generate({
model: "qwen-tts",
content: [{ type: "text", text: "欢迎使用语音合成功能。" }],
meta: {
voice_prompt: "一位沉稳自然的中文播音员,吐字清晰,语速适中",
},
});
await client.generate({
model: "qwen-audio-3.0-tts-flash",
content: [
{ type: "text", text: "这是一段长度足够并且表达清晰自然的语音合成文本。" },
{ type: "audio", source: { type: "url", url: "https://example.com/reference.mp3" } },
],
});await client.generate({
model: "higgs-tts",
content: [
{ type: "text", text: "使用多参考融合音色朗读这段文本。" },
{
type: "audio",
source: { type: "url", url: "https://example.com/reference-a.mp3" },
meta: { weight: 0.5 },
},
{
type: "audio",
source: { type: "url", url: "https://example.com/reference-b.mp3" },
meta: { weight: 0.5 },
},
],
});TTS reference audio only supports HTTP(S) URLs. Qwen always uses the single text block as the speech input.
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",
ratio: "16:9",
},
});Seedance frame and reference media video modes use meta.role with public URL media sources:
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" } },
],
});await client.generate({
model: "seedance-2-0-fast",
content: [
{ type: "text", text: "keep the subject from the image and the motion style from the video" },
{ type: "image", source: { type: "url", url: "https://example.com/subject.jpg" }, meta: { role: "reference_image" } },
{ type: "video", source: { type: "url", url: "https://example.com/motion.mp4" }, meta: { role: "reference_video" } },
],
});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: 1024x1024Set hidden: true on a declaration to hide the model from default discovery surfaces while keeping exact-ID lookup
and generation available. This is a discovery hint, not an authorization or runtime availability control.
listModels() returns the complete model catalog, including hidden models, so each user-facing discovery surface can
apply its own visibility policy without losing declaration data.
Adapter 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 };The content block type selects the media kind (image, video, or audio). The source type selects how that media is supplied (url or base64). When a model declares roles, meta.role selects the media's provider-specific purpose. When it also sets roleRequired, that role must be present.
Built-in adapters:
openai.imagesopenai.imageEditsopenai.audioSpeechgemini.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 {
GenerationProviderError,
GenerationTransportError,
GenerationValidationError,
} 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 GenerationTransportError) {
console.error("Provider transport failed", error.message);
console.error(error.details?.causeCode, error.details?.causeSyscall);
} else if (error instanceof GenerationProviderError) {
console.error("Provider failed", error.message);
console.error(error.status, error.details?.requestId, error.details?.code);
}
}