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",
});systemOne() uses @typesafe-ai/sdk@0.6.0 for requests, typed answers, cancellation, retries, and errors. Jev models are available through the existing local listModels(), getModel(), YAML export, and CLI discovery APIs.
import { choice, createGenerationClient, noul } from "@neta-art/generation";
const client = createGenerationClient({
apiKey: process.env.NETA_ROUTER_API_KEY!,
});
const result = await client.systemOne({
model: "jev-latest",
state: { ticket: "I was charged twice.", optional: null },
questions: {
refund: noul("Does the customer need a refund?"),
team: choice("Which team?", { billing: "Charges", technical: "Bugs" }),
},
});
console.log(result.answers.refund.noul);
console.log(result.answers.team.choice); // inferred as "billing" | "technical"
console.log(result.usage.input_tokens);Use systemOneResult() when you need billing metadata alongside the typed answers:
const { result, cost, requestId } = await client.systemOneResult({
model: "jev-latest",
state: { ticket: "I was charged twice." },
questions: {
refund: noul("Does the customer need a refund?"),
team: choice("Which team?", { billing: "Charges", technical: "Bugs" }),
},
});
console.log(result.answers.team.choice); // inferred as "billing" | "technical"
console.log(cost, requestId);This method returns Promise<SystemOneResultWithMetadata<Q>>. result is the official SystemOneResult<Q>; cost reads usage.cost from the response extensions. requestId uses the response body's request_id, followed by the x-request-id and x-oneapi-request-id response headers. Both metadata fields are optional and are omitted when unavailable; a reported zero cost is preserved. Each call captures its own metadata, and retries use the final response. Clients created from model files or directories also expose this method.
The default System One URL is ${baseUrl}/typesafe/v1/systemone, using the client's gateway token and fetch implementation. The gateway must enable the requested Jev model and native plugin route. To call OpenRouter directly, configure its System One base URL and use an OpenRouter key:
const direct = createGenerationClient({
apiKey: process.env.OPENROUTER_API_KEY!,
systemOne: {
baseURL: "https://openrouter.ai/api",
defaultModel: "jev-latest",
timeout: 60_000,
retry: { maxRetries: 0 },
},
});systemOne client options reuse the official TypeSafeClientConfig; per-call options reuse RequestOptions. apiKey and fetch come from the generation client. The official defaults apply for timeout and retries (10 seconds per attempt, 2 retries). Pass signal, timeout, retry, or headers as the second argument to systemOne() or systemOneResult() to override a call. Generation debug observes this transport with its existing redaction rules.
systemOne() returns the official APIPromise<SystemOneResult<Q>>, preserving question-name and criteria-key inference, .asResponse(), and .withResponse(). Request and response extensions remain available in the raw response. Gateway cost metadata can be read from X-OneAPI-Cost-Metadata; .withResponse().requestId follows the official SDK's x-typesafe-request-id header rule. Helpers noul, choice, and score, official request/result types, and official SDK error classes are re-exported from this package.
listModels() is synchronous and local, works without credentials, and includes jev-latest and jev-1.13.0 with category decision and systemOneExamples. It does not call the TypeSafe SDK's remote model listing, which cannot parse OpenRouter's model-list format. A local declaration describes SDK capabilities; it does not assert account access or server availability. Additional Jev aliases can be supplied through models using adapter: { type: "typesafe.systemOne" }. Use systemOne() or systemOneResult() for these models; generate(), generateResult(), and validate() accept the media generation request shape.
For a real-provider check, set JEV_API_KEY and JEV_BASE_URL (the System One API root, such as https://openrouter.ai/api or https://dev.new-api.talesofai.com/typesafe), then run pnpm test:live:jev. JEV_MODEL defaults to jev-latest.
decisions() uses @openrouter/sdk@1.4.25 (alpha.decisions.create()) for requests, response parsing, cancellation, retries, and errors. The model openai/gpt-6-luna-decisions is listed locally with category decision and decisionsExamples.
import { createGenerationClient } from "@neta-art/generation";
const client = createGenerationClient({
apiKey: process.env.NETA_ROUTER_API_KEY!,
});
const result = await client.decisions({
decisionsRequest: {
model: "openai/gpt-6-luna-decisions",
state: [
"Does this receipt show a duplicate charge?",
{ type: "image_url", image_url: { url: "data:image/png;base64,..." } },
],
questions: {
duplicate: { type: "noul", instructions: "Is the same charge listed twice?" },
team: { type: "choice", instructions: "Which team?", criteria: { billing: "Charges", technical: "Bugs" } },
},
},
});
const duplicate = result.answers.duplicate;
if (duplicate?.type === "noul") console.log(duplicate.noul);
console.log(result.usage.inputTokens);state is text, a JSON object, or an array of text and image_url parts. Images are PNG, JPEG, or WebP base64 data URLs, at most 128 per request; a request has at most 200 questions. Answers are the official DecisionsResponse union keyed by question name, so check type before reading noul, choice, or score.
decisionsResult() takes the same arguments and returns Promise<DecisionsResultWithMetadata>: result is the official DecisionsResponse, cost reads usage.cost from the response body, and requestId uses the body's request_id, followed by the x-request-id and x-oneapi-request-id response headers. Both metadata fields are omitted when unavailable.
The default Decisions URL is ${baseUrl}/openrouter/api/alpha/decisions, using the client's gateway token and fetch implementation. Gateway errors use OpenRouter's error format, so they are thrown as the official error classes (UnauthorizedResponseError, PaymentRequiredResponseError, BadGatewayResponseError, and so on) with error.metadata.request_id. To call OpenRouter directly, set the Decisions API root and use an OpenRouter key:
const direct = createGenerationClient({
apiKey: process.env.OPENROUTER_API_KEY!,
decisions: { serverURL: "https://openrouter.ai", timeoutMs: 60_000 },
});decisions client options reuse the official SDKOptions except apiKey, httpClient, and server. serverURL is applied to every call, because the official Decisions operation ignores the client-level server URL. Per-call options reuse the official RequestOptions (signal, timeoutMs, retries, headers, serverURL). Retries are off by default: a failed gateway call may already be billed, so retrying would charge again. Pass retryConfig (client) or retries (per call) to opt in. Official request/response types and error classes are re-exported from this package. Use decisions() or decisionsResult() for this model; generate(), generateResult(), and validate() reject it.
For a real-provider check, set LUNA_API_KEY and LUNA_SERVER_URL (the Decisions API root, such as https://openrouter.ai or https://new-api.talesofai.com/openrouter), then run pnpm test:live:luna. LUNA_MODEL defaults to openai/gpt-6-luna-decisions.
Doubao ASR 2.0 (volc.seedasr.auc) accepts exactly one audio URL and returns a text block. The gateway must support JSON requests to /v1/audio/transcriptions and have an enabled Volcengine speech channel.
const result = await client.generateResult({
model: "volc.seedasr.auc",
content: [
{ type: "audio", source: { type: "url", url: "https://example.com/recording.wav" } },
],
parameters: { audio_format: "wav", language: "zh-CN" },
});
console.log(result.content, result.requestId, result.cost);The adapter requests verbose_json and returns { type: "text", text, meta: { duration, segments } }. Duration and segment start/end are in seconds; each segment also contains text. Empty transcripts are valid. requestId and cost follow the normal generateResult behavior and are included when the gateway supplies them.
audio_format accepts wav, mp3, ogg or raw. It is inferred from the URL path extension when omitted; signed or extensionless URLs can specify it explicitly. language uses Volcengine codes such as zh-CN and en-US; omission retains the upstream defaults. The URL must remain accessible to Volcengine throughout processing. File uploads, base64, multiple audio blocks and realtime streaming are not supported by this model. Optional text content blocks are accepted and ignored, so callers can include a CLI prompt alongside the audio URL. Prompt text is never sent to the transcription service.
max_wait controls the SDK request timeout in seconds (default 330). It does not extend gateway limits: New API normally allows 300 seconds for transcription, while neta-router's generic upstream timeout defaults to 240 seconds. Configure the gateway for the expected recording workload. A client timeout does not cancel a submitted upstream job; the SDK does not automatically retry transcription requests.
Run pnpm example:speech-to-text with NETA_ROUTER_API_KEY and TRANSCRIPTION_AUDIO_URL in .env. Optional variables are NETA_ROUTER_BASE_URL, TRANSCRIPTION_AUDIO_FORMAT and TRANSCRIPTION_LANGUAGE. For a real provider check, export those variables and run pnpm test:live:audio-transcription; TRANSCRIPTION_EXPECTED_TEXT optionally checks a known passage.
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)),
},
});jev-latest(System One)jev-1.13.0(System One)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-videominimax-h3minimax-h3-unrestrictedseedance-2-0seedance-2-0-fastseedance-2-5suno_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_v5meshy
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" } },
],
});Seedance reference audio uses a public URL and meta.role: "reference_audio". Audio cannot be the only media input; include at least one image or video in the same request:
await client.generate({
model: "seedance-2-0",
content: [
{ type: "text", text: "keep the subject from the image and the rhythm from the audio" },
{ type: "image", source: { type: "url", url: "https://example.com/subject.jpg" }, meta: { role: "reference_image" } },
{ type: "audio", source: { type: "url", url: "https://example.com/reference.mp3" }, meta: { role: "reference_audio" } },
],
});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.
const preview = await client.generate({
model: "meshy",
content: [{ type: "text", text: "a wooden treasure chest with iron bands" }],
parameters: { operation: "text-to-3d", target_formats: ["glb", "usdz"] },
});
const [model] = preview;
if (!model) throw new Error("Meshy returned no files");
const refined = await client.generate({
model: "meshy",
content: [model, { type: "text", text: "weathered oak planks and dark iron" }],
parameters: { operation: "text-to-3d-refine", enable_pbr: true },
});Meshy is one model, meshy, with category 3d. The required parameters.operation selects the function:
operation |
Function |
|---|---|
text-to-image |
Generate an image from a text prompt |
image-to-image |
Edit 1 to 5 reference images with a text prompt |
text-to-3d |
Generate an untextured model from a text prompt |
text-to-3d-refine |
Texture a text-to-3d model |
image-to-3d |
Generate a textured model from one image |
multi-image-to-3d |
Generate a textured model from 1 to 4 views of an object |
retexture |
Apply a new texture to a model |
remesh |
Rebuild the mesh of a model and convert it to other formats |
rigging |
Add a skeleton to a humanoid model |
text-to-motion |
Generate a motion clip from a text description |
animation |
Apply preset actions or a motion clip to a rigged character |
operations in the model declaration lists the content inputs and parameters of each function. A request may use only
those of the selected operation, plus poll_interval and max_wait, which control how the SDK waits for the task.
Meshy request fields such as ai_model, target_formats, and enable_pbr go in parameters; the SDK does not send
operation, poll_interval, or max_wait to Meshy.
The result has one content block per file: 3d blocks first, starting with the main GLB, then image blocks for
thumbnails, texture maps, and generated images, then a video block when Meshy returns a preview video. Every block
carries meta.task_id, meta.task_type, and meta.field, the Meshy response field the file came from, such as
model_urls.glb or texture_urls[0].normal. 3d blocks also carry meta.format. An OBJ block carries meta.mtl_url,
a motion clip carries meta.duration_ms, and meta.task_expires_at is set when Meshy reports when the task expires.
To continue from an earlier result, pass its output block as input. A 3d block with meta.task_id references that
task:
text-to-3d-refinesends it aspreview_task_id. The block must come fromtext-to-3d.retexture,remesh, andriggingsend it asinput_task_id, and Meshy reads the main model of that task. They send the file URL asmodel_urlinstead when the block holds another file of the task, such asmodel_urls.pre_remeshed_glbor a rigged character, when Meshy does not accept the source task type inmeta.task_typefor that operation, or when the task has expired.animationsends theriggingblock asrig_task_id, and atext-to-motionblock marked withmeta.role: "motion"asmotion_task_id.
A 3d block without meta.task_id sends its URL as model_url. Meshy deletes a task 3 days after it finishes, at
meta.task_expires_at, and rejects later references to it. Refine and animation can only reference a task, so the SDK
rejects an expired source task before any request. The other operations send the file URL, which keeps working when it
points to a stored copy, such as the files neta-router archives.
const [character] = await client.generate({
model: "meshy",
content: [{ type: "3d", source: { type: "url", url: "https://example.com/character.glb" } }],
parameters: { operation: "rigging" },
});
const [motion] = await client.generate({
model: "meshy",
content: [{ type: "text", text: "a person jumps forward and lands softly" }],
parameters: { operation: "text-to-motion", duration: 3 },
});
if (!character || !motion) throw new Error("Meshy returned no files");
await client.generate({
model: "meshy",
content: [character, { ...motion, meta: { ...motion.meta, role: "motion" } }],
parameters: { operation: "animation" },
});image-to-3d and multi-image-to-3d take texture guide images as image blocks with meta.role: "texture". Image
and 3D inputs use URL sources.
Meshy recommends waiting at least 60 minutes for a task, so max_wait defaults to 3600 seconds. A status check that
fails with a network error, a timeout, 408, 429, or 5xx, or that breaks off while its body is read, is retried at the
next poll, up to 10 times in a row. Each request waits up to 240 seconds, below the 300-second response header limit of
Node's built-in fetch. After the task succeeds, neta-router archives the result files before it answers; when that takes
longer, the next poll waits for the archive that is already running. Errors raised after the task is submitted carry
its ID in details.task_id, with the original error as cause. generateResult() reports the usage.cost of the last
gateway response that has one. While the task runs, the gateway reports the estimate it holds; once the task succeeds,
it reports the cost settled from the credits Meshy consumed, except for a status check that arrives between recording
the success and settling the cost.
For a real gateway check, export NETA_ROUTER_API_KEY for a key that can use Meshy, optionally NETA_ROUTER_BASE_URL,
and run pnpm test:live:meshy. The check spends Meshy credits on a short chain of text-to-image, image-to-3D,
rigging, text-to-motion, animation, text-to-3D, refine, and remesh tasks.
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
category: image
pricing:
unit: image
min: 0.04
max: 0.73
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: 1024x1024Parameter specs have type string, number, integer, boolean, or array. Numbers and integers accept min,
max, and multipleOf. An array declares items, either a string with an optional enum or an integer with optional
min and max, plus optional min and max item counts and unique.
A model with several functions declares them in operations, and its string parameter operation selects one per
request, so operation is required unless it declares a default. The enum of operation lists the operation names
in the order of operations. Each operation may declare a title, a description, content.input, and parameters,
which add to the model's own and do not repeat its content types or parameter names:
schema: neta.generation.model.v1
model: meshy
category: 3d
adapter:
type: meshy.tasks
content:
input: []
parameters:
operation:
type: string
enum: [text-to-3d, remesh]
max_wait:
type: integer
optional: true
default: 3600
operations:
text-to-3d:
content:
input:
- type: text
required: true
remesh:
content:
input:
- type: 3d
required: true
parameters:
topology:
type: string
optional: true
enum: [quad, triangle]validate() checks the request against the model's content inputs and parameters plus those of the selected operation,
and returns that combined declaration as declaration. resolveGenerationOperation(declaration, parameters) returns
the combined declaration for the operation in parameters, and checks only parameters.operation.
A declaration may carry a display-only pricing block in USD. unit names what one charge covers
(image, second, request, or 1m_tokens); amount is the exact unit price, or a min/max
pair when the price depends on request parameters (resolution, duration, quality). The SDK does not
price or bill anything: pricing is declaration metadata for catalog surfaces, and actual charges
always come from the provider.
Set 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, audio, or 3d). 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.tasksmeshy.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);
}
}