ottoport 1.7.1 → 1.8.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 — as MCP tools, slash commands, or the bundled CLI.",
4
- "version": "1.7.1",
4
+ "version": "1.8.0",
5
5
  "author": {
6
6
  "name": "LITBOX LLC",
7
7
  "email": "support@ottoport.ai"
package/cli/ottoport.mjs CHANGED
@@ -5,6 +5,7 @@
5
5
  //
6
6
  // ottoport models [--modality chat|image|video]
7
7
  // ottoport chat "prompt" [--model claude-sonnet-5] [--system "..."] [--no-stream]
8
+ // [--video <url|file>] ask about a video (defaults to video-understanding)
8
9
  // ottoport image "prompt" [--model gpt-image-2] [--resolution 2k] [--ref a.png,b.png] [--out img.png]
9
10
  // ottoport video "prompt" [--model kling-3.0] [--duration 5] [--out clip.mp4]
10
11
  // ottoport speech "text" [--model gpt-4o-mini-tts] [--voice alloy] [--out speech.mp3]
@@ -17,7 +18,7 @@
17
18
  // The public CLI always targets https://ottoport.ai unless --url is passed
18
19
  // explicitly for local development or a self-hosted gateway.
19
20
 
20
- import { writeFile } from "node:fs/promises";
21
+ import { readFile, stat, writeFile } from "node:fs/promises";
21
22
 
22
23
  const BASE = "https://ottoport.ai";
23
24
 
@@ -152,11 +153,13 @@ async function cmdChat(prompt, flags) {
152
153
  const { baseUrl, apiKey } = config(flags);
153
154
  const messages = [];
154
155
  if (flags.system) messages.push({ role: "system", content: flags.system });
155
- messages.push({ role: "user", content: prompt });
156
+ const videos = await videoParts(flags);
157
+ messages.push({ role: "user", content: videos.length ? [{ type: "text", text: prompt }, ...videos] : prompt });
156
158
 
157
159
  const stream = flags.stream !== false;
158
160
  const body = {
159
- model: last(flags.model) || "claude-sonnet-5",
161
+ // A video question goes to the model built for it unless one is named.
162
+ model: last(flags.model) || (videos.length ? "video-understanding" : "claude-sonnet-5"),
160
163
  messages,
161
164
  stream,
162
165
  ...(flags.temperature ? { temperature: Number(last(flags.temperature)) } : {}),
@@ -202,6 +205,39 @@ async function cmdChat(prompt, flags) {
202
205
  process.stdout.write("\n");
203
206
  }
204
207
 
208
+ const VIDEO_TYPES = { ".mp4": "video/mp4", ".m4v": "video/mp4", ".mov": "video/quicktime", ".webm": "video/webm", ".mpeg": "video/mpeg", ".mpg": "video/mpeg" };
209
+
210
+ /**
211
+ * `--video`, once or several times (or comma-separated), as chat content parts.
212
+ *
213
+ * A URL is sent as-is: the gateway's transport fetches it, so it must be
214
+ * publicly reachable. A local file is inlined as a base64 data URL, which is
215
+ * convenient for trying a clip but costs a third more than the file on the
216
+ * wire — for anything large, host it and pass the URL.
217
+ */
218
+ async function videoParts(flags) {
219
+ const list = (Array.isArray(flags.video) ? flags.video : [flags.video])
220
+ .filter((value) => typeof value === "string")
221
+ .flatMap((value) => value.split(",").map((one) => one.trim()))
222
+ .filter(Boolean);
223
+ const parts = [];
224
+ for (const source of list) {
225
+ if (/^(https?:|data:)/i.test(source)) {
226
+ parts.push({ type: "video_url", video_url: { url: source } });
227
+ continue;
228
+ }
229
+ const info = await stat(source).catch(() => null);
230
+ if (!info?.isFile()) die(`--video ${source}: not a URL and no such file`);
231
+ const ext = source.slice(source.lastIndexOf(".")).toLowerCase();
232
+ const type = VIDEO_TYPES[ext];
233
+ if (!type) die(`--video ${source}: unsupported type ${ext || "(none)"}; use ${Object.keys(VIDEO_TYPES).join(", ")}`);
234
+ const mb = info.size / 1e6;
235
+ if (mb > 20) console.error(`ottoport: ${source} is ${mb.toFixed(0)}MB; inlined it will be ~${(mb * 4 / 3).toFixed(0)}MB and may be refused — host it and pass the URL instead`);
236
+ parts.push({ type: "video_url", video_url: { url: `data:${type};base64,${(await readFile(source)).toString("base64")}` } });
237
+ }
238
+ return parts;
239
+ }
240
+
205
241
  async function download(url, out) {
206
242
  const res = await fetch(url);
207
243
  if (!res.ok) die(`could not download ${url}: ${res.status}`);
@@ -377,6 +413,7 @@ Usage:
377
413
  ottoport models [--modality chat|image|video|tts|music] [--json]
378
414
  ottoport models <model-id> [--json] what that one model accepts
379
415
  ottoport chat "<prompt>" [--model claude-sonnet-5] [--system "..."] [--no-stream]
416
+ [--video <url|file>[,<url|file>…]] ask about a video
380
417
  [--temperature 0.7] [--max-tokens 512]
381
418
  ottoport image "<prompt>" [--model gpt-image-2] [--size 1024x1024 | --resolution 2k] [--quality high]
382
419
  [--aspect-ratio 16:9] [--n 1] [--seed 7]
@@ -409,6 +446,7 @@ Examples:
409
446
  ottoport models --modality image # prints what each one accepts
410
447
  ottoport models veo-3.1 # its modes, tiers, and every parameter
411
448
  ottoport chat "explain MCP in one line" --model claude-haiku-4.5
449
+ ottoport chat "break this clip down shot by shot" --video https://example.com/clip.mp4
412
450
  ottoport image "isometric city at dusk" --model gpt-image-2 --out city.png
413
451
  ottoport video "cinematic product reveal" --model seedance-2.0-fast --resolution 1080p
414
452
  ottoport video "the logo unfolds" --image start.png --last-frame end.png
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ottoport",
3
- "version": "1.7.1",
3
+ "version": "1.8.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": {