ottoport 1.15.1 → 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.1",
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-v5] [--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
@@ -118,6 +118,12 @@ raster, from a prompt alone: `output_format: "gif"` is an animation (`frame_coun
118
118
  request. A GIF picks its own frame size, so `size`, `resolution` and
119
119
  `aspect_ratio` do not apply to it.
120
120
 
121
+ ```sh
122
+ ottoport image "a paper boat bobbing on water, seamless loop" --format gif --wait --out boat.gif
123
+ ```
124
+
125
+ Asking for either format with no model named uses `gpt-image-2.6`.
126
+
121
127
  Which modes and tiers a model accepts differs per model, so
122
128
  `ottoport_list_models` — and `ottoport models` in the CLI — prints each one's
123
129
  menu beside its price. For one model in full, including every parameter it
@@ -153,9 +159,9 @@ https://ottoport.ai/docs#webhooks.
153
159
 
154
160
  - Image, video, and audio calls return **URLs**, and provider URLs expire.
155
161
  Download anything worth keeping.
156
- - Video generation takes a minute or more and the call blocks until the job is
157
- terminal, unless a webhook is given or `--no-wait` is passed. A retry is a
158
- 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.
159
165
  - `resolution` and `quality` are price multipliers, not formatting hints. Leave
160
166
  them unset to bill at the model's base tier.
161
167
  - Requests are billed against the prepaid balance on your OttoPort account.
@@ -177,6 +183,6 @@ ottoport job image <job-id> --wait --out lake.png
177
183
  ottoport image "a mountain lake" --wait --out lake.png
178
184
  ```
179
185
 
180
- 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.
181
187
 
182
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-v5] [--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
@@ -116,6 +116,7 @@ async function cmdModel(id, flags) {
116
116
  : caps.resolutions;
117
117
  console.log(`resolution: ${tiers.join(", ")}${caps.base_resolution ? ` (base ${caps.base_resolution})` : ""}`);
118
118
  }
119
+ if (caps?.outputFormats?.length) console.log(`outputs: ${caps.outputFormats.join(", ")}, besides images (--format)`);
119
120
  console.log("parameters:");
120
121
  for (const p of m.parameters ?? []) {
121
122
  console.log(` ${String(p.name).padEnd(21)}${p.type}${p.required ? ", required" : ""} — ${p.note}`);
@@ -150,7 +151,8 @@ function capabilityLine(caps) {
150
151
  return `accepts: ${caps.modes.join(", ")}${refs} · ${caps.resolutions.join("/")}`;
151
152
  }
152
153
  const inputs = caps.maxInputImages ? `up to ${caps.maxInputImages} input images` : "text only";
153
- return `accepts: ${inputs} · ${caps.resolutions.join("/")}`;
154
+ const formats = caps.outputFormats?.length ? ` · also outputs ${caps.outputFormats.join("/")}` : "";
155
+ return `accepts: ${inputs} · ${caps.resolutions.join("/")}${formats}`;
154
156
  }
155
157
 
156
158
  async function cmdChat(prompt, flags) {
@@ -268,12 +270,43 @@ function references(flags, name = "ref") {
268
270
  }
269
271
 
270
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
+
271
298
  async function resolveJob(kind, job, flags) {
272
- if (!job.id || !job.status) return job;
299
+ if (!job.id || !job.status || flags.wait !== true) return job;
273
300
  const { baseUrl, apiKey } = config(flags);
274
301
  // Always expose the ID before a caller opts into waiting.
275
- if (flags.wait === true) console.error(`job ${job.id} accepted`);
276
- 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
+ }
277
310
  await new Promise((resolve) => setTimeout(resolve, 3000));
278
311
  const res = await fetch(`${baseUrl}/api/v1/${JOB_PATHS[kind]}/${encodeURIComponent(job.id)}`, { headers: headers(apiKey, false) });
279
312
  if (!res.ok) die(await readError(res));
@@ -285,13 +318,16 @@ function printJob(kind, job) {
285
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));
286
319
  }
287
320
  async function cmdJob(kind, id, flags) {
288
- 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);
289
323
  const { baseUrl, apiKey } = config(flags);
290
324
  const res = await fetch(`${baseUrl}/api/v1/${JOB_PATHS[kind]}/${encodeURIComponent(id)}`, { headers: headers(apiKey, false) });
291
325
  if (!res.ok) die(await readError(res));
292
326
  const job = await resolveJob(kind, await res.json(), flags);
293
327
  printJob(kind, job);
294
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);
295
331
  const item = job.data?.[0];
296
332
  if (flags.out && (item?.asset_url || item?.url)) await download(item.asset_url || item.url, last(flags.out));
297
333
  }
@@ -300,9 +336,18 @@ async function cmdImage(prompt, flags) {
300
336
  // A layer-decomposition model needs only --image; the prompt is a caption.
301
337
  if (!prompt && !last(flags.image)) die("image requires a prompt: ottoport image \"a red fox\" (or --image <url> on a layer model)");
302
338
  const { baseUrl, apiKey } = config(flags);
339
+ const format = last(flags.format);
340
+ const wantsFile = format === "gif" || format === "svg";
341
+ // Checked before anything is spent: the file would hold one format under another's name.
342
+ const outExtension = String(last(flags.out) ?? "").match(/\.(png|jpe?g|webp|gif|svg)$/i)?.[1].toLowerCase();
343
+ if (wantsFile && outExtension && outExtension !== format) {
344
+ die(`--format ${format} returns a .${format} file, but --out names a .${outExtension} one. Change one of them.`);
345
+ }
346
+ checkSubmitFlags("image", flags);
303
347
  const body = {
304
348
  async: true,
305
- model: last(flags.model) || "gpt-image-2",
349
+ // The default model has no GIF or SVG output, so asking for one picks a model that does.
350
+ model: last(flags.model) || (wantsFile ? "gpt-image-2.6" : "gpt-image-2"),
306
351
  ...(prompt ? { prompt } : {}),
307
352
  ...(last(flags.layers) ? { num_layers: Number(last(flags.layers)) } : {}),
308
353
  ...(last(flags.size) ? { size: last(flags.size) } : {}),
@@ -341,6 +386,7 @@ async function cmdVideo(prompt, flags) {
341
386
  // A draft's 1080p render takes everything from the draft, the prompt included.
342
387
  const draftId = last(flags["draft-id"]);
343
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);
344
390
  const { baseUrl, apiKey } = config(flags);
345
391
  // Drafts are Seedance 2.5's, so a draft with no model named is one of its.
346
392
  const model = last(flags.model) || (flags.draft === true || draftId ? "seedance-2.5" : "kling-3.0");
@@ -372,15 +418,7 @@ async function cmdVideo(prompt, flags) {
372
418
  let job = await res.json();
373
419
  if (job.status === "failed") die(job.error || "video generation failed", 2);
374
420
  if (body.webhook_url) process.stderr.write(`webhook: ${body.webhook_url} will be notified when the job finishes\n`);
375
- if (flags.wait === true) {
376
- process.stderr.write(`job ${job.id} accepted; waiting for completion…\n`);
377
- while (job.status === "queued" || job.status === "processing") {
378
- await new Promise((resolve) => setTimeout(resolve, 3_000));
379
- const status = await fetch(`${baseUrl}/api/v1/videos/generations/${encodeURIComponent(job.id)}`, { headers: headers(apiKey, false) });
380
- if (!status.ok) die(await readError(status));
381
- job = await status.json();
382
- }
383
- }
421
+ job = await resolveJob("video", job, flags);
384
422
  printJob("video", job);
385
423
  if (job.status === "failed") die(job.error || "video generation failed", 2);
386
424
  const url = job.data?.[0]?.url;
@@ -434,10 +472,11 @@ async function cmdAudio(kind, prompt, flags) {
434
472
  // A sound-effects model scores a video, so the prompt is optional with one.
435
473
  const video = kind === "music" ? last(flags.video) : undefined;
436
474
  if (!prompt && !video) die(`${kind} requires a prompt: ottoport ${kind} "hello"`);
475
+ checkSubmitFlags(kind, flags);
437
476
  const { baseUrl, apiKey } = config(flags);
438
477
  const body = {
439
478
  async: true,
440
- model: last(flags.model) || (kind === "speech" ? "gpt-4o-mini-tts" : "suno-v5"),
479
+ model: last(flags.model) || (kind === "speech" ? "gpt-4o-mini-tts" : "suno-v6"),
441
480
  ...(prompt ? { prompt } : {}),
442
481
  ...(video ? { video_url: video } : {}),
443
482
  ...(last(flags.voice) ? { voice: last(flags.voice) } : {}),
@@ -527,8 +566,8 @@ Usage:
527
566
  [--temperature 0.7] [--max-tokens 512]
528
567
  ottoport image "<prompt>" [--model gpt-image-2] [--size 1024x1024 | --resolution 2k] [--quality high]
529
568
  [--aspect-ratio 16:9] [--n 1] [--seed 7]
530
- [--image <url>] [--ref <url>[,<url>…]] [--out file.png]
531
- ottoport image "<prompt>" --model gpt-image-2.6 --format gif|svg [--frames 8] [--frame-ms 200]
569
+ [--image <url>] [--ref <url>[,<url>…]] [--wait --out file.png]
570
+ ottoport image "<prompt>" --format gif|svg [--model gpt-image-2.6] [--frames 8] [--frame-ms 200] [--wait --out loop.gif]
532
571
  ottoport image --model qwen-image-layered --image <url> [--layers 4]
533
572
  split one image into RGBA layers (prompt optional)
534
573
  ottoport video "<prompt>" [--model kling-3.0] [--duration 5] [--resolution 1080p]
@@ -536,17 +575,18 @@ Usage:
536
575
  [--last-frame <url>] [--ref <url>[,<url>…]]
537
576
  [--ref-video <url>[,<url>…]] [--ref-audio <url>[,<url>…]]
538
577
  [--draft] [--webhook-url https://…] [--webhook-secret <s>]
539
- [--no-wait] [--out clip.mp4]
540
- 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]
541
580
  render a Seedance 2.5 draft (--draft, or the
542
581
  seedance-2.5-draft model) in 1080p, within 7 days
543
- ottoport video --model video-depth-anything --video <url> [--out depth.mp4]
582
+ ottoport video --model video-depth-anything --video <url> [--wait --out depth.mp4]
544
583
  turn a video (≤60s) into a grayscale depth video
545
584
  ottoport webhook status <job-id> [--json] delivery attempts and responses
546
585
  ottoport webhook retry <job-id> [--json] redeliver a finished job's webhook now
547
- ottoport speech "<text>" [--model gpt-4o-mini-tts] [--voice alloy] [--format mp3] [--out speech.mp3]
548
- ottoport music "<prompt>" [--model suno-v5] [--duration 15] [--format mp3] [--out song.mp3]
549
- 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]
550
590
  a soundtrack synced to the video, dialogue kept;
551
591
  set --duration to the clip's length (it bills)
552
592
  ottoport collections [list] [--json] collections in the key's project
@@ -562,6 +602,10 @@ Global flags:
562
602
  --webhook-secret <s> signs webhook deliveries (env OTTOPORT_WEBHOOK_SECRET)
563
603
  --collection <id> on image, video, speech, music: keep the results in
564
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
565
609
 
566
610
  Examples:
567
611
  export OTTOPORT_API_KEY=op-...
@@ -569,19 +613,19 @@ Examples:
569
613
  ottoport models veo-3.1 # its modes, tiers, and every parameter
570
614
  ottoport chat "explain MCP in one line" --model claude-haiku-4.5
571
615
  ottoport chat "break this clip down shot by shot" --video https://example.com/clip.mp4
572
- 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
573
617
  ottoport video "cinematic product reveal" --model seedance-2.0-fast --resolution 1080p
574
618
  ottoport video "the logo unfolds" --image start.png --last-frame end.png
575
- 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
576
620
  ottoport webhook retry 7c1e9a52-…
577
621
  ottoport image "her, on a beach" --ref face.png,style.png
578
622
  ottoport image --model seedream-5.0-layers --image https://…/poster.png
579
- ottoport speech "Welcome to OttoPort" --voice alloy --out welcome.mp3
580
- ottoport music "lo-fi focus beat" --duration 20 --out focus.mp3
581
- ottoport music "rain on a tin roof" --model mirelo-sfx-2.0 --duration 10 --format mp3 --out rain.mp3
582
- ottoport music --model mirelo-sfx-2.0 --video https://example.com/chase.mp4 --duration 8 --out chase.wav
583
- Media commands return jobs immediately. Add --wait to wait for results.
584
- 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
585
629
  ottoport collections create "Spring campaign"
586
630
  ottoport image "spring hero banner" --collection <collection-id>
587
631
  ottoport mcp
package/commands/image.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: Generate an image through the OttoPort gateway
3
- argument-hint: "<prompt> [--model <id>] [--resolution 2k] [--quality high] [--ref <url>…]"
3
+ argument-hint: "<prompt> [--model <id>] [--resolution 2k] [--quality high] [--ref <url>…] [--format gif|svg]"
4
4
  allowed-tools: mcp__ottoport__ottoport_generate_image, mcp__ottoport__ottoport_list_models, Bash(curl:*)
5
5
  ---
6
6
 
@@ -9,6 +9,12 @@ Generate an image with the `ottoport_generate_image` MCP tool.
9
9
  Request: $ARGUMENTS
10
10
 
11
11
  - Pass `model` only if the user named one; the tool defaults to `gpt-image-2`.
12
+ - An animated GIF or an SVG file comes from this tool, not from a video model:
13
+ set `output_format` to `gif` or `svg`, and the tool picks `gpt-image-2.6`. Only
14
+ that model and `gpt-image-2.6-flare` offer them, from a prompt alone, one file
15
+ per request. Put the motion and the loop in a GIF's prompt, and send no `size`,
16
+ `resolution` or `aspect_ratio` with it. `frame_count` is 2–16 (default 8; above
17
+ 8 bills double) and `frame_duration_ms` is 20–2000.
12
18
  - An edit of an existing image goes through `image_url`, not the prompt. Several
13
19
  references at once — a character plus the scene, a product plus a style — go
14
20
  through `reference_image_urls`.
@@ -16,7 +22,7 @@ Request: $ARGUMENTS
16
22
  `size` instead when the user named exact pixels. A higher tier costs more, so
17
23
  send one only when the request asks for it.
18
24
  - On the GPT Image models, `quality` buys compute rather than pixels — `low`,
19
- `medium` (the default), `high`, and `xhigh`/`max` on `gpt-image-2.5`. It is
25
+ `medium` (the default), `high`, and `xhigh`/`max` on `gpt-image-2.6` and `gpt-image-2.5`. It is
20
26
  billed steeply, so send it only when the user asked for a draft or a finish.
21
27
  - Models differ in how many references and which resolutions they take. If a
22
28
  request needs more than one reference, or 4k, check `ottoport_list_models`
@@ -11,7 +11,7 @@ backing track — or for sound effects, ambience or a soundtrack for a video, wi
11
11
  `model` `mirelo-sfx-2.0` (pass the video as `video_url`, and `duration` as its
12
12
  length); otherwise use `ottoport_generate_speech` to read the text aloud.
13
13
  Pass `voice` and `format` only when named — speech defaults to `gpt-4o-mini-tts`
14
- with the `alloy` voice, music to `suno-v5`.
14
+ with the `alloy` voice, music to `suno-v6`.
15
15
 
16
16
  Print the returned audio URL, and save it to disk with `curl -sSL` only if the
17
17
  user asked for a file.
package/mcp/server.mjs CHANGED
@@ -185,6 +185,9 @@ function accepts(capabilities) {
185
185
  if (Array.isArray(capabilities.resolutions) && capabilities.resolutions.length) {
186
186
  parts.push(capabilities.resolutions.join("/"));
187
187
  }
188
+ if (Array.isArray(capabilities.outputFormats) && capabilities.outputFormats.length) {
189
+ parts.push(`output_format ${capabilities.outputFormats.join("/")}`);
190
+ }
188
191
  return parts.length ? ` · accepts ${parts.join(", ")}` : "";
189
192
  }
190
193
 
@@ -217,6 +220,7 @@ async function describeModel(id) {
217
220
  : caps.resolutions;
218
221
  lines.push(`Resolutions: ${tiers.join(", ")}${caps.base_resolution ? ` (base ${caps.base_resolution})` : ""}`);
219
222
  }
223
+ if (caps?.outputFormats?.length) lines.push(`Output formats: ${caps.outputFormats.join(", ")}, besides images, via output_format`);
220
224
  lines.push("Parameters:");
221
225
  for (const p of m.parameters ?? []) {
222
226
  lines.push(` ${p.name} (${p.type}${p.required ? ", required" : ""}) — ${p.note}`);
@@ -329,7 +333,8 @@ async function generateImage({ prompt, model, collection, wait = false, ...rest
329
333
  // Queued as a job and polled: a 4K or layered image can take longer than
330
334
  // a proxy in front of the gateway holds a request open, and the hold is
331
335
  // then settled on what actually came back.
332
- body: JSON.stringify({ model: model || "gpt-image-2", ...(prompt ? { prompt } : {}), ...rest, ...withCollection(await collectionId(collection)), async: true }),
336
+ // The default model has no GIF or SVG output, so asking for one picks a model that does.
337
+ body: JSON.stringify({ model: model || (rest.output_format === "gif" || rest.output_format === "svg" ? "gpt-image-2.6" : "gpt-image-2"), ...(prompt ? { prompt } : {}), ...rest, ...withCollection(await collectionId(collection)), async: true }),
333
338
  });
334
339
  if (!res.ok) throw new Error(await gwError(res));
335
340
  let json = await res.json();
@@ -397,7 +402,7 @@ async function generateAudio(kind, { prompt, model, voice, duration, format, vid
397
402
  headers: { ...headers(), "idempotency-key": crypto.randomUUID() },
398
403
  body: JSON.stringify({
399
404
  async: true,
400
- model: model || (kind === "tts" ? "gpt-4o-mini-tts" : "suno-v5"),
405
+ model: model || (kind === "tts" ? "gpt-4o-mini-tts" : "suno-v6"),
401
406
  ...(prompt ? { prompt } : {}),
402
407
  ...(voice ? { voice } : {}),
403
408
  ...(video_url ? { video_url } : {}),
@@ -459,13 +464,13 @@ const TOOLS = [
459
464
  },
460
465
  {
461
466
  name: "ottoport_generate_image",
462
- description: "Generate an image through an OttoPort image model (GPT Image, Nano Banana, …). Text-to-image, editing from one reference, or several references at once. Also layer decomposition: qwen-image-layered and seedream-5.0-layers take `image_url` alone and return the image split into RGBA layers, one URL per layer, billed per layer. Vectors: ideogram-vector-1 returns editable SVG from a prompt (shape it with `aspect_ratio`, not `size`), and ideogram-vector-1-vectorize takes `image_url` alone and returns it traced as SVG. Which a model accepts is in its capabilities — call ottoport_list_models. Defaults to asynchronous submission: returns job_id and polling instructions. Use ottoport_get_job for results.",
467
+ description: "Generate an image through an OttoPort image model (GPT Image, Nano Banana, …). Text-to-image, editing from one reference, or several references at once. Also layer decomposition: qwen-image-layered and seedream-5.0-layers take `image_url` alone and return the image split into RGBA layers, one URL per layer, billed per layer. Animated GIFs and SVG files from a prompt: set output_format on gpt-image-2.6 or its Flare tier, rather than generating a video. Vectors: ideogram-vector-1 returns editable SVG from a prompt (shape it with `aspect_ratio`, not `size`), and ideogram-vector-1-vectorize takes `image_url` alone and returns it traced as SVG. Which a model accepts is in its capabilities — call ottoport_list_models. Defaults to asynchronous submission: returns job_id and polling instructions. Use ottoport_get_job for results.",
463
468
  inputSchema: {
464
469
  type: "object",
465
470
  properties: {
466
471
  wait: { type: "boolean", description: "Default false: return job immediately. True waits up to 45 seconds, then returns the same job for polling." },
467
472
  prompt: { type: "string", description: "What to generate. On a layer-decomposition model, an optional caption of the input image." },
468
- model: { type: "string", description: "Model id, e.g. gpt-image-2, nano-banana-2.1, nano-banana-pro. Default gpt-image-2." },
473
+ model: { type: "string", description: "Model id, e.g. gpt-image-2, nano-banana-2.1, nano-banana-pro. Default gpt-image-2, or gpt-image-2.6 when output_format is gif or svg." },
469
474
  size: { type: "string", description: 'Exact pixels, e.g. "1024x1024". Wins over `resolution`.' },
470
475
  // No `enum`: the vocabulary is "1k"/"2k"/"4k", but which of them a given
471
476
  // model actually offers differs per model, and an enum here reads as a
@@ -565,8 +570,8 @@ const TOOLS = [
565
570
  properties: {
566
571
  wait: { type: "boolean", description: "Default false: return job immediately. True waits up to 45 seconds, then returns the same job for polling." },
567
572
  prompt: { type: "string", description: "Music-generation prompt, or the sounds to make. Optional with video_url." },
568
- model: { type: "string", description: "Music model id. Default suno-v5; mirelo-sfx-2.0 for sound effects." },
569
- duration: { type: "number", description: "Requested duration in seconds." },
573
+ model: { type: "string", description: "Music model id. Default suno-v6; mirelo-sfx-2.0 for sound effects." },
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." },
570
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." },
571
576
  format: { type: "string", description: 'e.g. "mp3" or "wav".' },
572
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.1",
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": {
@@ -89,9 +89,10 @@ sensible default.
89
89
  | --- | --- | --- |
90
90
  | chat | `claude-sonnet-5` | `claude-haiku-4.5` (cheap), `gpt-5.5`, `gemini-3.5-flash` |
91
91
  | image | `gpt-image-2` | `nano-banana-2.1` (fast generation and edits), `nano-banana-pro` (high fidelity) |
92
+ | GIF or SVG file | `gpt-image-2.6`, with `output_format` on the image tool | `gpt-image-2.6-flare` (faster) |
92
93
  | video | `kling-3.0` | `seedance-2.0-fast` (cheap), `veo-3.1` (with audio) |
93
94
  | speech | `gpt-4o-mini-tts` | `eleven-v3` |
94
- | music | `suno-v5` | `lyria-2`, `mirelo-sfx-2.0` (sound effects, or a soundtrack for a video) |
95
+ | music | `suno-v6` | `lyria-2`, `mirelo-sfx-2.0` (sound effects, or a soundtrack for a video) |
95
96
 
96
97
  Never invent a model id. Call `ottoport_list_models` when unsure — the catalog
97
98
  changes, and a wrong id is a failed billable call. Rates come back in credits (the unit the ledger
@@ -122,12 +123,12 @@ export OTTOPORT_API_KEY=op-... # keys: https://ottoport.ai/api-keys
122
123
 
123
124
  ottoport models --modality image
124
125
  ottoport chat "explain MCP in one sentence" --model claude-haiku-4.5
125
- ottoport image "isometric city at dusk" --out city.png
126
- ottoport video "drone shot over a canyon" --duration 5 --out clip.mp4
127
- 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
128
129
  ```
129
130
 
130
- `--out` downloads the result; chat streams unless `--no-stream`.
131
+ `--wait --out` waits and downloads the result; chat streams unless `--no-stream`.
131
132
 
132
133
  ## Calling from code
133
134
 
@@ -158,6 +159,6 @@ ottoport job image <job-id> --wait --out lake.png
158
159
  ottoport image "a mountain lake" --wait --out lake.png
159
160
  ```
160
161
 
161
- 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.
162
163
 
163
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.