A Node CLI to submit a Sume video job: util.parseArgs and --dry-run
A Node script with util.parseArgs that validates duration, builds the /v1/videos request, and prints it with --dry-run before anything bills. Tested offline.

You can build a useful command-line tool for Sume video jobs with nothing but Node's built-in util.parseArgs: parse --prompt, --model, --duration and --dry-run, validate them against the model's limits, and either print the request that would be sent or send it with an Idempotency-Key. The dry run is the point. It lets a person or a coding agent read exactly what would be billed before any request leaves the machine, and it needs no network and no key.
This post uses only the request fields the Sume video models page and API reference document: model, prompt, duration, resolution and aspect_ratio, among others.
Why validate in the CLI?
Validation belongs in the tool, not in the server's error message. For example, Seedance 2.5 takes durations from 4 to 30 seconds, and Kling 3 tops out at 15. A wrong duration caught locally costs nothing and gives a clearer message than a round trip. Keep the table of limits small and cite where it came from, and if you add a model, copy its limits from the model docs instead of guessing. Unknown models should fail with a list of the known ones rather than being passed through.
What does the script look like?
const { parseArgs } = require("node:util");
const LIMITS = { "seedance-2.5": [4, 30], "kling-3": [4, 15], "wan-3.0": [2, 30] };
function build(argv, env = {}) {
const { values: v } = parseArgs({ args: argv, options: {
prompt: { type: "string" }, model: { type: "string", default: "seedance-2.5" },
duration: { type: "string", default: "5" }, "dry-run": { type: "boolean" },
key: { type: "string" } } });
const range = LIMITS[v.model];
if (!range) throw new Error("unknown model; try " + Object.keys(LIMITS).join(", "));
const duration = Number(v.duration);
if (!v.prompt || !(duration >= range[0] && duration <= range[1]))
throw new Error(`need --prompt and a duration of ${range[0]}-${range[1]} s for ${v.model}`);
const key = v.key || "cli-" + require("node:crypto").createHash("sha256")
.update(v.model + duration + v.prompt).digest("hex").slice(0, 16);
return { url: "https://api.sume.com/v1/videos", dryRun: !!v["dry-run"],
headers: { Authorization: "Bearer " + (env.SUME_API_KEY ? "***" : "(unset)"), "Idempotency-Key": key },
body: { model: v.model, prompt: v.prompt, duration } };
}
const a = build(["--prompt", "a red kite over dunes", "--duration", "8", "--dry-run"]);
console.log(JSON.stringify(a));
if (!a.dryRun || a.body.duration !== 8) process.exit(1);
const b = build(["--prompt", "a red kite over dunes", "--duration", "8"]);
if (b.headers["Idempotency-Key"] !== a.headers["Idempotency-Key"]) process.exit(1);
for (const bad of [["--prompt", "x", "--duration", "40"], ["--prompt", "x", "--model", "veo"]]) {
try { build(bad); process.exit(1); } catch (e) { console.log("rejected:", e.message); }
}What do the idempotency and masking choices do?
Two choices are worth noting. First, the idempotency key is derived from the model, duration and prompt when you do not pass one, so running the same command twice sends the same key and the server returns the original job for a same-key, same-payload retry, instead of making a second paid one. A different payload under the same key returns 409 idempotency_conflict, so the script is safe to re-run after a crash but does not hide a real change. Second, the dry run masks the key: it prints *** and never the secret, which makes the output safe to paste into a ticket or an agent transcript.
Sume jobs and results describes how to follow a job after a real submit: poll the status URL, obey a next_poll_after_seconds hint when present, and fetch the result once it is ready. The video route's own 202 response carries an id and a polling URL.
| Flag | Default | Effect |
|---|---|---|
| --model | seedance-2.5 | Chooses the limits table row |
| --duration | 5 | Checked against the model range |
| --dry-run | off | Prints the request and sends nothing |
| --key | derived hash | Overrides the Idempotency-Key |
Checking limits against the catalog instead of a constant
The three-row LIMITS table is a convenience, not a source of truth. The authoritative values come from GET /v1/videos/models, whose descriptors list supported_durations, supported_resolutions and supported_aspect_ratios for each model, and the API answers a value outside those lists with 400 unsupported_capability rather than quietly changing it. A sturdier CLI fetches the catalog once, caches it for the session and validates against the descriptor, falling back to the embedded table only for an offline dry run.
Remember which fields the route refuses outright. seed, size and a non-empty provider.options return 400 unsupported_parameter, so a flag for any of them would only add a way to fail. If a user asks for a resolution flag, check it against the chosen model's list first: Seedance 2.5 offers 480p, 720p and 1080p, Kling 3 offers 720p and 1080p, and MiniMax H3 offers 480p and 768p.
How do I turn it into a sender?
To make it send, add a fetch call after the dry-run branch with the printed headers, using the real SUME_API_KEY from the environment and refusing to start when it is empty. Then save the job id to disk before you do anything else, poll status_url honouring next_poll_after_seconds, and fetch the result when it reports ready. A client timeout does not cancel the job, and it still bills, so a retry should reuse the same key rather than build a new one. The errors that deserve a retry are listed in Sume errors and credits; the ones that do not, such as a missing credit balance, need a human.
Keep the tool small. Extra flags for resolution and aspect ratio are easy to add, but each one needs a check against the model's documented options, so add them only when you need them.
Sources
Related posts
More in Developers
- Node fetch to Sume with Ideogram 4.5: branch on status 200 or 202
POST /v1/images returns 200 with data[].url or 202 with a job envelope. A Node 22 fetch sample that checks res.status, with a 40 second abort signal.
- Node fetch worker pool: submit transcription jobs with retry-after
A 30-line Node 18+ worker pool that posts Sume STT jobs, sleeps for retry-after on 429, and sends an Idempotency-Key per clip. Tested against a stub.
- Nova Canvas boto3 read timeout vs Sume's 30 second wait and 202
The AWS SDK read timeout is 60 s and Amazon suggests 300 s for Nova Canvas. Sume's /v1/images waits 30 s, then returns a 202 job. Handle both in Python.
- Nova Canvas cfgScale 1.1 to 10 vs Sume: no guidance field
Nova Canvas has a cfgScale from 1.1 to 10, default 6.5. Sume rejects fields a model does not list. How to get the same effect with wording and model choice.
Written by Sume