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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +17 -11
- package/cli/ottoport.mjs +80 -36
- package/commands/image.md +8 -2
- package/commands/speech.md +1 -1
- package/mcp/server.mjs +11 -6
- package/package.json +1 -1
- package/skills/ottoport/SKILL.md +7 -6
|
@@ -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.
|
|
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://…]
|
|
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-
|
|
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
|
|
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
|
|
157
|
-
|
|
158
|
-
|
|
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`
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
276
|
-
|
|
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
|
|
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
|
-
|
|
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-
|
|
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
|
|
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
|
-
[--
|
|
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-
|
|
549
|
-
|
|
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
|
|
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
|
|
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`
|
package/commands/speech.md
CHANGED
|
@@ -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-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
569
|
-
duration: { type: "number", description: "Requested
|
|
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.
|
|
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": {
|
package/skills/ottoport/SKILL.md
CHANGED
|
@@ -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-
|
|
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`
|
|
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.
|