ottoport 1.15.2 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ottoport",
3
3
  "description": "One API for every model. Call chat, image, video, speech and music models through the OttoPort gateway \u2014 as MCP tools, slash commands, or the bundled CLI.",
4
- "version": "1.15.2",
4
+ "version": "1.16.0",
5
5
  "author": {
6
6
  "name": "LITBOX LLC",
7
7
  "email": "support@ottoport.ai"
package/README.md CHANGED
@@ -21,23 +21,23 @@ ottoport chat "<prompt>" [--model claude-sonnet-5] [--system "..."] [--no-strea
21
21
  [--temperature 0.7] [--max-tokens 512]
22
22
  ottoport image "<prompt>" [--model gpt-image-2] [--size 1024x1024 | --resolution 2k]
23
23
  [--quality high] [--aspect-ratio 16:9] [--n 1] [--seed 7]
24
- [--image <url>] [--ref <url>[,<url>…]] [--out file.png]
24
+ [--image <url>] [--ref <url>[,<url>…]] [--wait --out file.png]
25
25
  ottoport image --model qwen-image-layered --image <url> [--layers 4]
26
26
  ottoport video "<prompt>" [--model kling-3.0] [--duration 5] [--resolution 1080p]
27
27
  [--aspect-ratio 16:9] [--image <url>] [--last-frame <url>]
28
- [--ref <url>[,<url>…]] [--out clip.mp4]
29
- [--webhook-url https://…] [--no-wait]
28
+ [--ref <url>[,<url>…]] [--wait --out clip.mp4]
29
+ [--webhook-url https://…]
30
30
  ottoport webhook status|retry <job-id> # delivery attempts / redeliver now
31
- ottoport speech "<text>" [--model gpt-4o-mini-tts] [--voice alloy] [--out speech.mp3]
32
- ottoport music "<prompt>" [--model suno-v6] [--duration 15] [--out song.mp3]
31
+ ottoport speech "<text>" [--model gpt-4o-mini-tts] [--voice alloy] [--wait --out speech.mp3]
32
+ ottoport music "<prompt>" [--model suno-v6] [--duration 15] [--wait --out song.mp3]
33
33
  ottoport music --model mirelo-sfx-2.0 --video <url> [--duration 8] # soundtrack for a video
34
34
 
35
35
  ottoport install <host> # wire the MCP server into an agent
36
36
  ottoport mcp # run the MCP server on stdio
37
37
  ```
38
38
 
39
- Chat streams by default. Image, video and audio commands print the result URL;
40
- `--out` downloads it. `--url` and `--key` override the environment for a single
39
+ Chat streams by default. Image, video and audio commands return a job at once;
40
+ `--wait` waits for the result URL and `--wait --out` also downloads it. `--url` and `--key` override the environment for a single
41
41
  call.
42
42
 
43
43
  Every command is a thin client over the public REST API, so anything the CLI
@@ -159,9 +159,9 @@ https://ottoport.ai/docs#webhooks.
159
159
 
160
160
  - Image, video, and audio calls return **URLs**, and provider URLs expire.
161
161
  Download anything worth keeping.
162
- - Video generation takes a minute or more and the call blocks until the job is
163
- terminal, unless a webhook is given or `--no-wait` is passed. A retry is a
164
- second billable generation, not a resumption.
162
+ - Video generation takes a minute or more. The call returns a job at once and
163
+ only blocks with `--wait`. A retry is a second billable generation, not a
164
+ resumption.
165
165
  - `resolution` and `quality` are price multipliers, not formatting hints. Leave
166
166
  them unset to bill at the model's base tier.
167
167
  - Requests are billed against the prepaid balance on your OttoPort account.
@@ -183,6 +183,6 @@ ottoport job image <job-id> --wait --out lake.png
183
183
  ottoport image "a mountain lake" --wait --out lake.png
184
184
  ```
185
185
 
186
- Use job kinds `image`, `video`, `speech`, or `music`. `--out` downloads only when a result is available; otherwise query the returned job with `--wait --out`. Pending and failed job responses retain the task ID and status route.
186
+ Use job kinds `image`, `video`, `speech`, or `music`. On generation commands `--out` needs `--wait`, and is refused before anything is submitted without it. `ottoport job <kind> <id> --out file` saves a finished job and exits 3 if it is still running. `--wait` gives up after 30 minutes by default (`--timeout <seconds>`) and exits 3; the job keeps running and can be resumed with `ottoport job`. Pending and failed job responses retain the task ID and status route.
187
187
 
188
188
  MCP generation tools return `job_id`, `status`, `status_path`, and the next tool call. Query with `ottoport_get_job({kind, job_id})`. Optional `wait: true` waits up to 45 seconds per MCP call, returning the same job if still pending. Querying a job does not submit another generation. Existing scripts that expected immediate URLs should add `--wait`, or adopt job polling.
package/cli/ottoport.mjs CHANGED
@@ -6,10 +6,10 @@
6
6
  // ottoport models [--modality chat|image|video]
7
7
  // ottoport chat "prompt" [--model claude-sonnet-5] [--system "..."] [--no-stream]
8
8
  // [--video <url|file>] ask about a video (defaults to video-understanding)
9
- // ottoport image "prompt" [--model gpt-image-2] [--resolution 2k] [--ref a.png,b.png] [--out img.png]
10
- // ottoport video "prompt" [--model kling-3.0] [--duration 5] [--out clip.mp4]
11
- // ottoport speech "text" [--model gpt-4o-mini-tts] [--voice alloy] [--out speech.mp3]
12
- // ottoport music "prompt" [--model suno-v6] [--duration 15] [--out song.mp3]
9
+ // ottoport image "prompt" [--model gpt-image-2] [--resolution 2k] [--ref a.png,b.png] [--wait --out img.png]
10
+ // ottoport video "prompt" [--model kling-3.0] [--duration 5] [--wait --out clip.mp4]
11
+ // ottoport speech "text" [--model gpt-4o-mini-tts] [--voice alloy] [--wait --out speech.mp3]
12
+ // ottoport music "prompt" [--model suno-v6] [--duration 15] [--wait --out song.mp3]
13
13
  // ottoport music --model mirelo-sfx-2.0 --video <url> [--duration 8]
14
14
  // ottoport collections [list | create "name"]
15
15
  // ottoport mcp
@@ -270,12 +270,43 @@ function references(flags, name = "ref") {
270
270
  }
271
271
 
272
272
  const JOB_PATHS = { image: "images/generations", video: "videos/generations", speech: "audio/speech", music: "audio/music" };
273
+ const PENDING = ["queued", "processing"];
274
+ // Long enough for a slow video queue; the job itself carries on past it either way.
275
+ const DEFAULT_WAIT_SECONDS = 1800;
276
+
277
+ function waitSeconds(flags) {
278
+ if (flags.timeout === undefined) return DEFAULT_WAIT_SECONDS;
279
+ const seconds = Number(last(flags.timeout));
280
+ if (!(seconds > 0)) die("--timeout takes a number of seconds above 0");
281
+ return seconds;
282
+ }
283
+
284
+ /**
285
+ * Jobs are asynchronous and only --wait blocks. With neither a wait nor a later
286
+ * process to save it, --out could never produce a file, so it is refused before
287
+ * anything is spent.
288
+ */
289
+ function checkSubmitFlags(kind, flags) {
290
+ waitSeconds(flags);
291
+ if (!flags.out || flags.wait === true) return;
292
+ const file = last(flags.out);
293
+ die(`--out needs --wait: jobs run asynchronously, so no file would be saved. Nothing was submitted.
294
+ wait and save now: add --wait to this command
295
+ save it later: drop --out, then run: ottoport job ${kind} <job-id> --wait --out ${file}`);
296
+ }
297
+
273
298
  async function resolveJob(kind, job, flags) {
274
- if (!job.id || !job.status) return job;
299
+ if (!job.id || !job.status || flags.wait !== true) return job;
275
300
  const { baseUrl, apiKey } = config(flags);
276
301
  // Always expose the ID before a caller opts into waiting.
277
- if (flags.wait === true) console.error(`job ${job.id} accepted`);
278
- while (flags.wait === true && ["queued", "processing"].includes(job.status)) {
302
+ console.error(`job ${job.id} accepted`);
303
+ const seconds = waitSeconds(flags);
304
+ const deadline = Date.now() + seconds * 1000;
305
+ while (PENDING.includes(job.status)) {
306
+ if (Date.now() >= deadline) {
307
+ printJob(kind, job);
308
+ die(`job ${job.id} is still ${job.status} after ${seconds}s and keeps running. Resume with: ottoport job ${kind} ${job.id} --wait${flags.out ? ` --out ${last(flags.out)}` : ""}`, 3);
309
+ }
279
310
  await new Promise((resolve) => setTimeout(resolve, 3000));
280
311
  const res = await fetch(`${baseUrl}/api/v1/${JOB_PATHS[kind]}/${encodeURIComponent(job.id)}`, { headers: headers(apiKey, false) });
281
312
  if (!res.ok) die(await readError(res));
@@ -287,13 +318,16 @@ function printJob(kind, job) {
287
318
  console.log(JSON.stringify({ ...job, job_id: job.id, kind, status_path: `/api/v1/${JOB_PATHS[kind]}/${encodeURIComponent(job.id)}`, next: `ottoport job ${kind} ${job.id}` }, null, 2));
288
319
  }
289
320
  async function cmdJob(kind, id, flags) {
290
- if (!Object.hasOwn(JOB_PATHS, kind) || !id) die("usage: ottoport job image|video|speech|music <id> [--wait] [--out file]");
321
+ if (!Object.hasOwn(JOB_PATHS, kind) || !id) die("usage: ottoport job image|video|speech|music <id> [--wait [--timeout seconds]] [--out file]");
322
+ waitSeconds(flags);
291
323
  const { baseUrl, apiKey } = config(flags);
292
324
  const res = await fetch(`${baseUrl}/api/v1/${JOB_PATHS[kind]}/${encodeURIComponent(id)}`, { headers: headers(apiKey, false) });
293
325
  if (!res.ok) die(await readError(res));
294
326
  const job = await resolveJob(kind, await res.json(), flags);
295
327
  printJob(kind, job);
296
328
  if (job.status === "failed") die(job.error || "generation failed", 2);
329
+ // Looking at a job never blocks unless asked, but a file that was named and not saved must not read as success.
330
+ if (flags.out && PENDING.includes(job.status)) die(`job ${id} is still ${job.status}, so nothing was saved. Wait and save with: ottoport job ${kind} ${id} --wait --out ${last(flags.out)}`, 3);
297
331
  const item = job.data?.[0];
298
332
  if (flags.out && (item?.asset_url || item?.url)) await download(item.asset_url || item.url, last(flags.out));
299
333
  }
@@ -309,6 +343,7 @@ async function cmdImage(prompt, flags) {
309
343
  if (wantsFile && outExtension && outExtension !== format) {
310
344
  die(`--format ${format} returns a .${format} file, but --out names a .${outExtension} one. Change one of them.`);
311
345
  }
346
+ checkSubmitFlags("image", flags);
312
347
  const body = {
313
348
  async: true,
314
349
  // The default model has no GIF or SVG output, so asking for one picks a model that does.
@@ -351,6 +386,7 @@ async function cmdVideo(prompt, flags) {
351
386
  // A draft's 1080p render takes everything from the draft, the prompt included.
352
387
  const draftId = last(flags["draft-id"]);
353
388
  if (!prompt && !last(flags.video) && !draftId) die("video requires a prompt: ottoport video \"a drone shot\" (or --video <url> on a depth model)");
389
+ checkSubmitFlags("video", flags);
354
390
  const { baseUrl, apiKey } = config(flags);
355
391
  // Drafts are Seedance 2.5's, so a draft with no model named is one of its.
356
392
  const model = last(flags.model) || (flags.draft === true || draftId ? "seedance-2.5" : "kling-3.0");
@@ -382,15 +418,7 @@ async function cmdVideo(prompt, flags) {
382
418
  let job = await res.json();
383
419
  if (job.status === "failed") die(job.error || "video generation failed", 2);
384
420
  if (body.webhook_url) process.stderr.write(`webhook: ${body.webhook_url} will be notified when the job finishes\n`);
385
- if (flags.wait === true) {
386
- process.stderr.write(`job ${job.id} accepted; waiting for completion…\n`);
387
- while (job.status === "queued" || job.status === "processing") {
388
- await new Promise((resolve) => setTimeout(resolve, 3_000));
389
- const status = await fetch(`${baseUrl}/api/v1/videos/generations/${encodeURIComponent(job.id)}`, { headers: headers(apiKey, false) });
390
- if (!status.ok) die(await readError(status));
391
- job = await status.json();
392
- }
393
- }
421
+ job = await resolveJob("video", job, flags);
394
422
  printJob("video", job);
395
423
  if (job.status === "failed") die(job.error || "video generation failed", 2);
396
424
  const url = job.data?.[0]?.url;
@@ -444,6 +472,7 @@ async function cmdAudio(kind, prompt, flags) {
444
472
  // A sound-effects model scores a video, so the prompt is optional with one.
445
473
  const video = kind === "music" ? last(flags.video) : undefined;
446
474
  if (!prompt && !video) die(`${kind} requires a prompt: ottoport ${kind} "hello"`);
475
+ checkSubmitFlags(kind, flags);
447
476
  const { baseUrl, apiKey } = config(flags);
448
477
  const body = {
449
478
  async: true,
@@ -537,7 +566,7 @@ Usage:
537
566
  [--temperature 0.7] [--max-tokens 512]
538
567
  ottoport image "<prompt>" [--model gpt-image-2] [--size 1024x1024 | --resolution 2k] [--quality high]
539
568
  [--aspect-ratio 16:9] [--n 1] [--seed 7]
540
- [--image <url>] [--ref <url>[,<url>…]] [--out file.png]
569
+ [--image <url>] [--ref <url>[,<url>…]] [--wait --out file.png]
541
570
  ottoport image "<prompt>" --format gif|svg [--model gpt-image-2.6] [--frames 8] [--frame-ms 200] [--wait --out loop.gif]
542
571
  ottoport image --model qwen-image-layered --image <url> [--layers 4]
543
572
  split one image into RGBA layers (prompt optional)
@@ -546,17 +575,18 @@ Usage:
546
575
  [--last-frame <url>] [--ref <url>[,<url>…]]
547
576
  [--ref-video <url>[,<url>…]] [--ref-audio <url>[,<url>…]]
548
577
  [--draft] [--webhook-url https://…] [--webhook-secret <s>]
549
- [--no-wait] [--out clip.mp4]
550
- ottoport video --draft-id <job-id> [--model seedance-2.5] [--out clip.mp4]
578
+ [--wait --out clip.mp4]
579
+ ottoport video --draft-id <job-id> [--model seedance-2.5] [--wait --out clip.mp4]
551
580
  render a Seedance 2.5 draft (--draft, or the
552
581
  seedance-2.5-draft model) in 1080p, within 7 days
553
- ottoport video --model video-depth-anything --video <url> [--out depth.mp4]
582
+ ottoport video --model video-depth-anything --video <url> [--wait --out depth.mp4]
554
583
  turn a video (≤60s) into a grayscale depth video
555
584
  ottoport webhook status <job-id> [--json] delivery attempts and responses
556
585
  ottoport webhook retry <job-id> [--json] redeliver a finished job's webhook now
557
- ottoport speech "<text>" [--model gpt-4o-mini-tts] [--voice alloy] [--format mp3] [--out speech.mp3]
558
- ottoport music "<prompt>" [--model suno-v6] [--duration 15] [--format mp3] [--out song.mp3]
559
- ottoport music ["<prompt>"] --model mirelo-sfx-2.0 --video <url> [--duration 8] [--out sfx.wav]
586
+ ottoport speech "<text>" [--model gpt-4o-mini-tts] [--voice alloy] [--format mp3] [--wait --out speech.mp3]
587
+ ottoport music "<prompt>" [--model suno-v6] [--duration 15] [--format mp3] [--wait --out song.mp3]
588
+ On Suno, --duration is a target length; the price per request stays flat
589
+ ottoport music ["<prompt>"] --model mirelo-sfx-2.0 --video <url> [--duration 8] [--wait --out sfx.wav]
560
590
  a soundtrack synced to the video, dialogue kept;
561
591
  set --duration to the clip's length (it bills)
562
592
  ottoport collections [list] [--json] collections in the key's project
@@ -572,6 +602,10 @@ Global flags:
572
602
  --webhook-secret <s> signs webhook deliveries (env OTTOPORT_WEBHOOK_SECRET)
573
603
  --collection <id> on image, video, speech, music: keep the results in
574
604
  that collection instead of letting them expire
605
+ --wait on image, video, speech, music, job: block until the job
606
+ finishes. Exit 3 if it is still running at the timeout
607
+ --timeout <seconds> how long --wait blocks (default ${DEFAULT_WAIT_SECONDS})
608
+ --out <file> save the result; generation commands need --wait with it
575
609
 
576
610
  Examples:
577
611
  export OTTOPORT_API_KEY=op-...
@@ -579,19 +613,19 @@ Examples:
579
613
  ottoport models veo-3.1 # its modes, tiers, and every parameter
580
614
  ottoport chat "explain MCP in one line" --model claude-haiku-4.5
581
615
  ottoport chat "break this clip down shot by shot" --video https://example.com/clip.mp4
582
- ottoport image "isometric city at dusk" --model gpt-image-2 --out city.png
616
+ ottoport image "isometric city at dusk" --model gpt-image-2 --wait --out city.png
583
617
  ottoport video "cinematic product reveal" --model seedance-2.0-fast --resolution 1080p
584
618
  ottoport video "the logo unfolds" --image start.png --last-frame end.png
585
- ottoport video "drone over fjords" --webhook-url https://example.com/hooks/ottoport --no-wait
619
+ ottoport video "drone over fjords" --webhook-url https://example.com/hooks/ottoport
586
620
  ottoport webhook retry 7c1e9a52-…
587
621
  ottoport image "her, on a beach" --ref face.png,style.png
588
622
  ottoport image --model seedream-5.0-layers --image https://…/poster.png
589
- ottoport speech "Welcome to OttoPort" --voice alloy --out welcome.mp3
590
- ottoport music "lo-fi focus beat" --duration 20 --out focus.mp3
591
- ottoport music "rain on a tin roof" --model mirelo-sfx-2.0 --duration 10 --format mp3 --out rain.mp3
592
- ottoport music --model mirelo-sfx-2.0 --video https://example.com/chase.mp4 --duration 8 --out chase.wav
593
- Media commands return jobs immediately. Add --wait to wait for results.
594
- ottoport job image|video|speech|music <id> [--wait] [--out file]
623
+ ottoport speech "Welcome to OttoPort" --voice alloy --wait --out welcome.mp3
624
+ ottoport music "lo-fi focus beat" --duration 20 --wait --out focus.mp3
625
+ ottoport music "rain on a tin roof" --model mirelo-sfx-2.0 --duration 10 --format mp3 --wait --out rain.mp3
626
+ ottoport music --model mirelo-sfx-2.0 --video https://example.com/chase.mp4 --duration 8 --wait --out chase.wav
627
+ Media commands return jobs immediately. Add --wait to wait for the result, and --wait --out to save it.
628
+ ottoport job image|video|speech|music <id> [--wait] [--out file] --out alone saves a finished job, exit 3 if it is not done
595
629
  ottoport collections create "Spring campaign"
596
630
  ottoport image "spring hero banner" --collection <collection-id>
597
631
  ottoport mcp
package/mcp/server.mjs CHANGED
@@ -571,7 +571,7 @@ const TOOLS = [
571
571
  wait: { type: "boolean", description: "Default false: return job immediately. True waits up to 45 seconds, then returns the same job for polling." },
572
572
  prompt: { type: "string", description: "Music-generation prompt, or the sounds to make. Optional with video_url." },
573
573
  model: { type: "string", description: "Music model id. Default suno-v6; mirelo-sfx-2.0 for sound effects." },
574
- duration: { type: "number", description: "Requested duration in seconds." },
574
+ duration: { type: "number", description: "Requested length in seconds, billed per second. On the Suno models it is a target length instead: each request has one flat price, and the song may run longer or shorter." },
575
575
  video_url: { type: "string", description: "mirelo-sfx-2.0 only: an http(s) video to score. Set duration to its length — duration is what bills." },
576
576
  format: { type: "string", description: 'e.g. "mp3" or "wav".' },
577
577
  collection: { type: "string", description: "Save the result for good into this collection — its id or exact name (see ottoport_list_collections). Without it the file expires after a few days. Needs collections unlocked on the account. Defaults to OTTOPORT_COLLECTION when set." },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ottoport",
3
- "version": "1.15.2",
3
+ "version": "1.16.0",
4
4
  "description": "Claude Code plugin, CLI and MCP server for OttoPort — one OpenAI-compatible API for every LLM, image, video, and speech model.",
5
5
  "homepage": "https://ottoport.ai",
6
6
  "repository": {
@@ -123,12 +123,12 @@ export OTTOPORT_API_KEY=op-... # keys: https://ottoport.ai/api-keys
123
123
 
124
124
  ottoport models --modality image
125
125
  ottoport chat "explain MCP in one sentence" --model claude-haiku-4.5
126
- ottoport image "isometric city at dusk" --out city.png
127
- ottoport video "drone shot over a canyon" --duration 5 --out clip.mp4
128
- ottoport speech "Welcome to OttoPort" --voice alloy --out welcome.mp3
126
+ ottoport image "isometric city at dusk" --wait --out city.png
127
+ ottoport video "drone shot over a canyon" --duration 5 --wait --out clip.mp4
128
+ ottoport speech "Welcome to OttoPort" --voice alloy --wait --out welcome.mp3
129
129
  ```
130
130
 
131
- `--out` downloads the result; chat streams unless `--no-stream`.
131
+ `--wait --out` waits and downloads the result; chat streams unless `--no-stream`.
132
132
 
133
133
  ## Calling from code
134
134
 
@@ -159,6 +159,6 @@ ottoport job image <job-id> --wait --out lake.png
159
159
  ottoport image "a mountain lake" --wait --out lake.png
160
160
  ```
161
161
 
162
- Use job kinds `image`, `video`, `speech`, or `music`. `--out` downloads only when a result is available; otherwise query the returned job with `--wait --out`. Pending and failed job responses retain the task ID and status route.
162
+ Use job kinds `image`, `video`, `speech`, or `music`. On generation commands `--out` needs `--wait`, and is refused before anything is submitted without it. `ottoport job <kind> <id> --out file` saves a finished job and exits 3 if it is still running. `--wait` gives up after 30 minutes by default (`--timeout <seconds>`) and exits 3; the job keeps running and can be resumed with `ottoport job`. Pending and failed job responses retain the task ID and status route.
163
163
 
164
164
  MCP generation tools return `job_id`, `status`, `status_path`, and the next tool call. Query with `ottoport_get_job({kind, job_id})`. Optional `wait: true` waits up to 45 seconds per MCP call, returning the same job if still pending. Querying a job does not submit another generation. Existing scripts that expected immediate URLs should add `--wait`, or adopt job polling.