ottoport 1.4.1 → 1.5.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 +6 -0
- package/cli/ottoport.mjs +10 -3
- package/mcp/server.mjs +23 -10
- package/package.json +1 -1
- package/skills/ottoport/SKILL.md +8 -1
|
@@ -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 — as MCP tools, slash commands, or the bundled CLI.",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.5.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "LITBOX LLC",
|
|
7
7
|
"email": "support@ottoport.ai"
|
package/README.md
CHANGED
|
@@ -22,6 +22,7 @@ ottoport chat "<prompt>" [--model claude-sonnet-5] [--system "..."] [--no-strea
|
|
|
22
22
|
ottoport image "<prompt>" [--model gpt-image-2] [--size 1024x1024 | --resolution 2k]
|
|
23
23
|
[--aspect-ratio 16:9] [--n 1] [--seed 7]
|
|
24
24
|
[--image <url>] [--ref <url>[,<url>…]] [--out file.png]
|
|
25
|
+
ottoport image --model qwen-image-layered --image <url> [--layers 4]
|
|
25
26
|
ottoport video "<prompt>" [--model kling-3.0] [--duration 5] [--resolution 1080p]
|
|
26
27
|
[--aspect-ratio 16:9] [--image <url>] [--last-frame <url>]
|
|
27
28
|
[--ref <url>[,<url>…]] [--out clip.mp4]
|
|
@@ -86,6 +87,11 @@ first-and-last-frame (`image_url` + `last_frame_url`), and reference-to-video
|
|
|
86
87
|
(`reference_image_urls`). Both take a `resolution`: `480p`…`4k` for video,
|
|
87
88
|
`1k`/`2k`/`4k` for images, and a tier above a model's base rate costs more.
|
|
88
89
|
|
|
90
|
+
Two image models take a picture apart instead: `qwen-image-layered` and
|
|
91
|
+
`seedream-5.0-layers` split `image_url` into RGBA layers and return one URL per
|
|
92
|
+
layer, bottom first (the prompt is an optional caption; `num_layers`, 2–10, is
|
|
93
|
+
honoured by Qwen only). Each layer returned bills separately.
|
|
94
|
+
|
|
89
95
|
Which modes and tiers a model accepts differs per model, so
|
|
90
96
|
`ottoport_list_models` — and `ottoport models` in the CLI — prints each one's
|
|
91
97
|
menu beside its price. For one model in full, including every parameter it
|
package/cli/ottoport.mjs
CHANGED
|
@@ -225,11 +225,13 @@ function references(flags) {
|
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
async function cmdImage(prompt, flags) {
|
|
228
|
-
|
|
228
|
+
// A layer-decomposition model needs only --image; the prompt is a caption.
|
|
229
|
+
if (!prompt && !last(flags.image)) die("image requires a prompt: ottoport image \"a red fox\" (or --image <url> on a layer model)");
|
|
229
230
|
const { baseUrl, apiKey } = config(flags);
|
|
230
231
|
const body = {
|
|
231
232
|
model: last(flags.model) || "gpt-image-2",
|
|
232
|
-
prompt,
|
|
233
|
+
...(prompt ? { prompt } : {}),
|
|
234
|
+
...(last(flags.layers) ? { num_layers: Number(last(flags.layers)) } : {}),
|
|
233
235
|
...(last(flags.size) ? { size: last(flags.size) } : {}),
|
|
234
236
|
...(last(flags.resolution) ? { resolution: last(flags.resolution) } : {}),
|
|
235
237
|
...(last(flags["aspect-ratio"]) ? { aspect_ratio: flags["aspect-ratio"] } : {}),
|
|
@@ -246,7 +248,9 @@ async function cmdImage(prompt, flags) {
|
|
|
246
248
|
if (!res.ok) die(await readError(res));
|
|
247
249
|
const json = await res.json();
|
|
248
250
|
const urls = (json.data ?? []).map((d) => d.url).filter(Boolean);
|
|
249
|
-
|
|
251
|
+
// A layer stack names each line, so `headline` can be found among eight URLs.
|
|
252
|
+
(json.data ?? []).filter((d) => d.url).forEach((d) =>
|
|
253
|
+
console.log(d.layer ? `layer ${d.layer.index}${d.layer.name ? ` (${d.layer.name})` : ""}: ${d.url}` : d.url));
|
|
250
254
|
if (last(flags.out) && urls[0]) await download(urls[0], last(flags.out));
|
|
251
255
|
}
|
|
252
256
|
|
|
@@ -334,6 +338,8 @@ Usage:
|
|
|
334
338
|
ottoport image "<prompt>" [--model gpt-image-2] [--size 1024x1024 | --resolution 2k]
|
|
335
339
|
[--aspect-ratio 16:9] [--n 1] [--seed 7]
|
|
336
340
|
[--image <url>] [--ref <url>[,<url>…]] [--out file.png]
|
|
341
|
+
ottoport image --model qwen-image-layered --image <url> [--layers 4]
|
|
342
|
+
split one image into RGBA layers (prompt optional)
|
|
337
343
|
ottoport video "<prompt>" [--model kling-3.0] [--duration 5] [--resolution 1080p]
|
|
338
344
|
[--aspect-ratio 16:9] [--seed 7] [--image <url>]
|
|
339
345
|
[--last-frame <url>] [--ref <url>[,<url>…]]
|
|
@@ -358,6 +364,7 @@ Examples:
|
|
|
358
364
|
ottoport video "cinematic product reveal" --model seedance-2.0-fast --resolution 1080p
|
|
359
365
|
ottoport video "the logo unfolds" --image start.png --last-frame end.png
|
|
360
366
|
ottoport image "her, on a beach" --ref face.png,style.png
|
|
367
|
+
ottoport image --model seedream-5.0-layers --image https://…/poster.png
|
|
361
368
|
ottoport speech "Welcome to OttoPort" --voice alloy --out welcome.mp3
|
|
362
369
|
ottoport music "lo-fi focus beat" --duration 20 --out focus.mp3
|
|
363
370
|
ottoport mcp
|
package/mcp/server.mjs
CHANGED
|
@@ -59,7 +59,10 @@ function accepts(capabilities) {
|
|
|
59
59
|
if (Array.isArray(capabilities.modes) && capabilities.modes.length) {
|
|
60
60
|
parts.push(capabilities.modes.join("/"));
|
|
61
61
|
}
|
|
62
|
-
if (
|
|
62
|
+
if (capabilities.output === "layers") {
|
|
63
|
+
// Not a reference image: the one input IS the job, and the answer is a stack.
|
|
64
|
+
parts.push(`one image_url, split into RGBA layers${capabilities.maxLayers ? ` (num_layers 2–${capabilities.maxLayers})` : " (count chosen by the model)"}`);
|
|
65
|
+
} else if (typeof capabilities.maxInputImages === "number") {
|
|
63
66
|
parts.push(capabilities.maxInputImages > 0 ? `up to ${capabilities.maxInputImages} reference images` : "text only");
|
|
64
67
|
}
|
|
65
68
|
if (typeof capabilities.maxReferenceImages === "number") {
|
|
@@ -87,7 +90,10 @@ async function describeModel(id) {
|
|
|
87
90
|
const refs = caps.maxReferenceImages ? ` (up to ${caps.maxReferenceImages} reference images)` : "";
|
|
88
91
|
lines.push(`Modes: ${caps.modes.join(", ")}${refs}`);
|
|
89
92
|
}
|
|
90
|
-
if (
|
|
93
|
+
if (caps?.output === "layers") {
|
|
94
|
+
lines.push("Output: the input image split into RGBA layers — one `data` entry per layer, bottom first, each with a `layer` object (index; name, z_index and bounding_box where the model reports them). Billed per layer returned.");
|
|
95
|
+
lines.push(`Input images: exactly one, in image_url. The prompt is an optional caption.${caps.maxLayers ? ` num_layers picks the count, 2–${caps.maxLayers}.` : " The model picks the layer count."}`);
|
|
96
|
+
} else if (typeof caps?.maxInputImages === "number") {
|
|
91
97
|
lines.push(`Input images: ${caps.maxInputImages > 0 ? `up to ${caps.maxInputImages}` : "none — text-to-image only"}`);
|
|
92
98
|
}
|
|
93
99
|
if (caps?.resolutions?.length) {
|
|
@@ -146,7 +152,10 @@ async function chat({ prompt, model, system, temperature, max_tokens }) {
|
|
|
146
152
|
}
|
|
147
153
|
|
|
148
154
|
async function generateImage({ prompt, model, ...rest }) {
|
|
149
|
-
|
|
155
|
+
// A layer-decomposition model splits `image_url` and takes the prompt as an
|
|
156
|
+
// optional caption; every other image model needs the prompt. The gateway
|
|
157
|
+
// knows which is which and says so.
|
|
158
|
+
if (!prompt && !rest.image_url) throw new Error("`prompt` is required (or `image_url` alone, on a layer-decomposition model)");
|
|
150
159
|
const res = await fetch(`${BASE}/api/v1/images/generations`, {
|
|
151
160
|
method: "POST",
|
|
152
161
|
headers: { ...headers(), "idempotency-key": crypto.randomUUID() },
|
|
@@ -156,12 +165,16 @@ async function generateImage({ prompt, model, ...rest }) {
|
|
|
156
165
|
// live on the gateway and unreachable from any MCP client, silently. The
|
|
157
166
|
// gateway validates against the model's capabilities and says what it
|
|
158
167
|
// does not accept; this layer has no business having an opinion.
|
|
159
|
-
body: JSON.stringify({ model: model || "gpt-image-2", prompt, ...rest }),
|
|
168
|
+
body: JSON.stringify({ model: model || "gpt-image-2", ...(prompt ? { prompt } : {}), ...rest }),
|
|
160
169
|
});
|
|
161
170
|
if (!res.ok) throw new Error(await gwError(res));
|
|
162
171
|
const json = await res.json();
|
|
163
|
-
|
|
164
|
-
|
|
172
|
+
// A layer stack prints one line per layer with what the model called it, so
|
|
173
|
+
// the agent can pick "headline" out of eight URLs without opening them.
|
|
174
|
+
const lines = (json.data ?? []).filter((d) => d.url).map((d) =>
|
|
175
|
+
d.layer ? `layer ${d.layer.index}${d.layer.name ? ` (${d.layer.name})` : ""}: ${d.url}` : d.url,
|
|
176
|
+
);
|
|
177
|
+
return lines.length ? lines.join("\n") : "(no image returned)";
|
|
165
178
|
}
|
|
166
179
|
|
|
167
180
|
async function generateVideo({ prompt, model, ...rest }) {
|
|
@@ -243,11 +256,11 @@ const TOOLS = [
|
|
|
243
256
|
},
|
|
244
257
|
{
|
|
245
258
|
name: "ottoport_generate_image",
|
|
246
|
-
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. Which a model accepts is in its capabilities — call ottoport_list_models. Returns image URL(s).",
|
|
259
|
+
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. Which a model accepts is in its capabilities — call ottoport_list_models. Returns image URL(s).",
|
|
247
260
|
inputSchema: {
|
|
248
261
|
type: "object",
|
|
249
262
|
properties: {
|
|
250
|
-
prompt: { type: "string" },
|
|
263
|
+
prompt: { type: "string", description: "What to generate. On a layer-decomposition model, an optional caption of the input image." },
|
|
251
264
|
model: { type: "string", description: "Model id, e.g. gpt-image-2, nano-banana-2, nano-banana-pro. Default gpt-image-2." },
|
|
252
265
|
size: { type: "string", description: 'Exact pixels, e.g. "1024x1024". Wins over `resolution`.' },
|
|
253
266
|
// No `enum`: the vocabulary is "1k"/"2k"/"4k", but which of them a given
|
|
@@ -259,10 +272,10 @@ const TOOLS = [
|
|
|
259
272
|
aspect_ratio: { type: "string", description: 'e.g. "16:9". Read only when `size` is absent.' },
|
|
260
273
|
n: { type: "number" },
|
|
261
274
|
seed: { type: "number" },
|
|
262
|
-
image_url: { type: "string", description: "One reference image, for editing and image-to-image." },
|
|
275
|
+
image_url: { type: "string", description: "One reference image, for editing and image-to-image — or, on a layer-decomposition model, the image to split." },
|
|
263
276
|
reference_image_urls: { type: "array", items: { type: "string" }, description: "Several references — a character sheet, a product plus a scene — up to the model's maxInputImages. Call ottoport_list_models to see it." },
|
|
277
|
+
num_layers: { type: "number", description: "Layer models that let you choose (qwen-image-layered, 2–10): how many RGBA layers to split into. Each bills separately. seedream-5.0-layers decides for itself and refuses this." },
|
|
264
278
|
},
|
|
265
|
-
required: ["prompt"],
|
|
266
279
|
},
|
|
267
280
|
handler: generateImage,
|
|
268
281
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ottoport",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.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
|
"license": "MIT",
|
package/skills/ottoport/SKILL.md
CHANGED
|
@@ -16,7 +16,7 @@ and do not call the HTTP API, when a tool covers the job:
|
|
|
16
16
|
| --- | --- |
|
|
17
17
|
| `ottoport_list_models` | The catalog: what each model is for, and its rate in credits. Filter with `modality`, or pass `model` for ONE model in full — its modes, its resolution tiers with prices, and every parameter it reads. |
|
|
18
18
|
| `ottoport_chat` | `prompt`, plus optional `model`, `system`, `temperature`, `max_tokens`. |
|
|
19
|
-
| `ottoport_generate_image` | `prompt`, plus optional `model`, `size` or `resolution`, `aspect_ratio`, `n`, `seed`, `image_url` (one reference), `reference_image_urls` (several). |
|
|
19
|
+
| `ottoport_generate_image` | `prompt`, plus optional `model`, `size` or `resolution`, `aspect_ratio`, `n`, `seed`, `image_url` (one reference), `reference_image_urls` (several). Layer decomposition too: `model` `qwen-image-layered` or `seedream-5.0-layers` with `image_url` alone (prompt optional, `num_layers` on Qwen) returns one URL per RGBA layer. |
|
|
20
20
|
| `ottoport_generate_video` | `prompt`, plus optional `model`, `duration`, `resolution`, `aspect_ratio`, `seed`, `image_url` (first frame), `last_frame_url`, `reference_image_urls`. |
|
|
21
21
|
| `ottoport_generate_speech` | `prompt` (the text), plus optional `model`, `voice`, `format`. |
|
|
22
22
|
| `ottoport_generate_music` | `prompt`, plus optional `model`, `duration`, `format`. |
|
|
@@ -40,6 +40,13 @@ prints each one's menu beside its price:
|
|
|
40
40
|
Images are the same idea with one axis: `image_url` for a single edit, or
|
|
41
41
|
`reference_image_urls` for several at once, up to that model's limit.
|
|
42
42
|
|
|
43
|
+
Two image models do not generate at all — they take an image apart.
|
|
44
|
+
`qwen-image-layered` and `seedream-5.0-layers` split `image_url` into RGBA
|
|
45
|
+
layers and return one URL per layer, bottom of the stack first; the prompt is
|
|
46
|
+
an optional caption of the image, `num_layers` (2–10) is honoured by Qwen only,
|
|
47
|
+
and **each layer returned bills separately**. Reach for them when the user wants
|
|
48
|
+
a poster, ad or product shot as editable parts rather than a new picture.
|
|
49
|
+
|
|
43
50
|
`resolution` is separate from all of it, and **it is priced** — a higher tier
|
|
44
51
|
costs more. The vocabulary is `"480p"`/`"720p"`/`"1080p"`/`"2k"`/`"4k"` for
|
|
45
52
|
video and `"1k"`/`"2k"`/`"4k"` for images, but **no model offers all of it**:
|