ottoport 1.2.3 → 1.3.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 +22 -0
- package/.mcp.json +12 -0
- package/README.md +48 -152
- package/cli/install.mjs +169 -0
- package/cli/ottoport.mjs +9 -0
- package/commands/chat.md +16 -0
- package/commands/image.md +14 -0
- package/commands/models.md +14 -0
- package/commands/setup.md +22 -0
- package/commands/speech.md +15 -0
- package/commands/video.md +17 -0
- package/package.json +5 -39
- package/skills/ottoport/SKILL.md +99 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ottoport",
|
|
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.3.0",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "LITBOX LLC",
|
|
7
|
+
"email": "support@ottoport.ai"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://ottoport.ai",
|
|
10
|
+
"repository": "https://github.com/Jet1217/OttoPort",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"ai",
|
|
14
|
+
"llm",
|
|
15
|
+
"api-gateway",
|
|
16
|
+
"openai-compatible",
|
|
17
|
+
"image-generation",
|
|
18
|
+
"video-generation",
|
|
19
|
+
"text-to-speech",
|
|
20
|
+
"mcp"
|
|
21
|
+
]
|
|
22
|
+
}
|
package/.mcp.json
ADDED
package/README.md
CHANGED
|
@@ -1,167 +1,63 @@
|
|
|
1
|
-
# OttoPort
|
|
1
|
+
# OttoPort for Claude Code
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
official API, a reseller, or a backup provider without changing the client
|
|
7
|
-
contract.
|
|
3
|
+
One API for every model. This plugin bundles three things into a single
|
|
4
|
+
install: the OttoPort **MCP server**, the **`ottoport` skill**, and a set of
|
|
5
|
+
**slash commands** for chat, image, video, speech, and music generation.
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
brand, soft shadows, hairline borders.
|
|
7
|
+
## Install
|
|
11
8
|
|
|
12
|
-
|
|
9
|
+
```
|
|
10
|
+
/plugin marketplace add https://ottoport.ai/plugin/marketplace.json
|
|
11
|
+
/plugin install ottoport@ottoport
|
|
12
|
+
```
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
- **Supabase** — auth, accounts, API keys, usage ledger
|
|
17
|
-
- **Stripe** — prepaid credits / plans
|
|
18
|
-
- Provider SDKs: `openai`, `@anthropic-ai/sdk`
|
|
14
|
+
Then set your key in the shell that launches Claude Code — the MCP server reads
|
|
15
|
+
the environment it inherits, so a key exported inside a session is too late:
|
|
19
16
|
|
|
20
|
-
|
|
17
|
+
```bash
|
|
18
|
+
export OTTOPORT_API_KEY=op-... # create one at https://ottoport.ai/api-keys
|
|
19
|
+
export OTTOPORT_BASE_URL=https://ottoport.ai # optional; this is the default
|
|
20
|
+
```
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
Run `/ottoport:setup` to confirm the key, the gateway, and the MCP tools are all
|
|
23
|
+
live.
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
| --- | --- | --- |
|
|
26
|
-
| `POST /v1/chat/completions` | LLM | OpenAI-compatible, streaming supported. Routes GPT → OpenAI, Claude → Anthropic. |
|
|
27
|
-
| `POST /v1/images/generations` | Image | `{ model, prompt, size?, n?, image_url? }` → `{ data: [{ url }] }`. |
|
|
28
|
-
| `POST /v1/videos/generations` | Video | Submits `{ model, prompt, duration?, aspect_ratio?, webhook_url?, webhook_secret? }` and returns `202` with an OttoPort job ID. |
|
|
29
|
-
| `GET /v1/videos/generations` | Video | Lists the authenticated project's persisted video generation history. |
|
|
30
|
-
| `GET /v1/videos/generations/:id` | Video | Fetches the latest persisted and provider status. |
|
|
31
|
-
| `POST /v1/videos/generations/:id/webhook` | Video | Retries a terminal job's webhook delivery. |
|
|
32
|
-
| `POST /v1/audio/speech` | Speech | `{ model, input, voice?, format? }`; directly calls OpenAI, ElevenLabs, or Google. |
|
|
33
|
-
| `GET /v1/models` | — | Catalog with pricing + modality. |
|
|
25
|
+
## Slash commands
|
|
34
26
|
|
|
35
|
-
|
|
27
|
+
| Command | What it does |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| `/ottoport:models` | The catalog with live pricing, filterable by modality. |
|
|
30
|
+
| `/ottoport:chat` | Send a prompt to any chat model (GPT, Claude, Gemini, Kimi, GLM …). |
|
|
31
|
+
| `/ottoport:image` | Generate or edit an image. |
|
|
32
|
+
| `/ottoport:video` | Submit a video job and wait for the URL. |
|
|
33
|
+
| `/ottoport:speech` | Read text aloud, or generate music. |
|
|
34
|
+
| `/ottoport:setup` | Diagnose key / gateway / MCP wiring. |
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
types.ts normalized request/response shapes
|
|
44
|
-
providers/
|
|
45
|
-
index.ts adapter registry + model route policy + fallback
|
|
46
|
-
openrouter.ts unified LLM transport (except Kimi)
|
|
47
|
-
moonshot.ts official Kimi chat API
|
|
48
|
-
openai.ts official OpenAI image API
|
|
49
|
-
google.ts official Gemini image API
|
|
50
|
-
video.ts official Veo / Kling / Volcengine video APIs
|
|
51
|
-
modelark.ts official Seedance 2.0 / Seedream 5.0 APIs
|
|
52
|
-
atlascloud.ts optional Atlas Cloud transport for Seedance / Seedream
|
|
53
|
-
legnext.ts Midjourney image generation via Legnext
|
|
54
|
-
audio.ts official OpenAI / ElevenLabs / Google speech APIs
|
|
55
|
-
```
|
|
36
|
+
## MCP tools
|
|
37
|
+
|
|
38
|
+
The bundled server exposes `ottoport_list_models`, `ottoport_chat`,
|
|
39
|
+
`ottoport_generate_image`, `ottoport_generate_video`, `ottoport_generate_speech`,
|
|
40
|
+
and `ottoport_generate_music`. Any agent in the session can call them directly —
|
|
41
|
+
the slash commands are just convenient entry points over the same tools.
|
|
56
42
|
|
|
57
|
-
|
|
58
|
-
model's `provider` remains its original vendor; runtime transport selection is
|
|
59
|
-
separate. Configure model-specific transport routes and fallbacks in
|
|
60
|
-
`libs/gateway/provider-config.ts`.
|
|
61
|
-
For synchronous requests, routes are tried in order on provider failures. Video
|
|
62
|
-
jobs persist their chosen route so later status polling always reaches the API
|
|
63
|
-
that owns the upstream job ID.
|
|
43
|
+
## CLI
|
|
64
44
|
|
|
65
|
-
|
|
45
|
+
The same package ships the `ottoport` CLI for terminal and CI use:
|
|
66
46
|
|
|
67
47
|
```bash
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
48
|
+
npm install -g ottoport
|
|
49
|
+
ottoport models --modality video
|
|
50
|
+
ottoport image "isometric city at dusk" --out city.png
|
|
71
51
|
```
|
|
72
52
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
[`sql/generation-queue-migration.sql`](sql/generation-queue-migration.sql), then
|
|
85
|
-
[`sql/account-concurrency-limits-migration.sql`](sql/account-concurrency-limits-migration.sql), after the project migration.
|
|
86
|
-
Video submissions are persisted and queued, so the returned job ID can be queried after a process restart. Add an HTTPS
|
|
87
|
-
`webhook_url` and a caller-held `webhook_secret` to receive a signed `video.completed` or `video.failed` event.
|
|
88
|
-
The signature header is `x-ottoport-signature` and uses `HMAC-SHA256(secret, "timestamp.payload")`.
|
|
89
|
-
|
|
90
|
-
Set `GENERATION_JOBS_CRON_SECRET`, then configure a trusted scheduler to call
|
|
91
|
-
`GET /api/internal/generation-jobs/dispatch` every minute with `Authorization: Bearer <secret>`.
|
|
92
|
-
The dispatcher atomically leases queued jobs, limits in-flight jobs per account and provider, retries provider
|
|
93
|
-
failures with exponential backoff, and sends terminal webhooks even when the API client is no longer polling.
|
|
94
|
-
|
|
95
|
-
### Dynamic account concurrency
|
|
96
|
-
|
|
97
|
-
Video concurrency is evaluated per account at the moment a job is claimed.
|
|
98
|
-
`concurrency_limit_tiers` maps successful paid cash top-ups in the current UTC
|
|
99
|
-
month to capacities; `account_concurrency_overrides` provides expiring
|
|
100
|
-
per-account adjustments. Update either table in Supabase to change capacity
|
|
101
|
-
without deploying. The migration seeds starter, growth, scale, and enterprise
|
|
102
|
-
defaults; treat their thresholds and limits as product configuration.
|
|
103
|
-
|
|
104
|
-
For example, to temporarily grant one account five in-flight video jobs for a
|
|
105
|
-
week, upsert `account_concurrency_overrides` with `video_concurrency = 5` and
|
|
106
|
-
an `expires_at` timestamp. The next worker claim uses that value immediately.
|
|
107
|
-
|
|
108
|
-
### Request reservations and idempotency
|
|
109
|
-
|
|
110
|
-
Chat and video calls require an `Idempotency-Key` request header. OttoPort atomically reserves the worst-case
|
|
111
|
-
cost before submitting upstream, then settles the reservation from actual usage. Reuse the same key only when
|
|
112
|
-
retrying the identical request; a new key represents a new billable generation. Configure the gateway caps in
|
|
113
|
-
`.env.example` before production traffic.
|
|
114
|
-
|
|
115
|
-
### Stripe prepaid wallet
|
|
116
|
-
|
|
117
|
-
Set `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`, and `NEXT_PUBLIC_APP_URL`.
|
|
118
|
-
The required billing migration adds paid-vs-bonus
|
|
119
|
-
balances, a tamper-resistant wallet ledger, saved payment methods, and
|
|
120
|
-
automatic top-up settings.
|
|
121
|
-
|
|
122
|
-
For an environment where `billing-wallet-migration.sql` was already applied,
|
|
123
|
-
run [`sql/auto-topup-safety-migration.sql`](sql/auto-topup-safety-migration.sql)
|
|
124
|
-
as well. Then configure a Stripe webhook for:
|
|
125
|
-
|
|
126
|
-
- `checkout.session.completed`
|
|
127
|
-
- `checkout.session.async_payment_succeeded`
|
|
128
|
-
- `payment_intent.succeeded`
|
|
129
|
-
|
|
130
|
-
pointing to `https://your-domain/api/billing/webhook`. The dashboard creates a
|
|
131
|
-
one-time Checkout Session (not a subscription). Users can also save a card and
|
|
132
|
-
enable automatic top-up after explicit consent. Credits are added only after
|
|
133
|
-
the webhook verifies Stripe's raw-body signature and records the Checkout
|
|
134
|
-
Session or PaymentIntent ID, so webhook retries cannot duplicate a top-up. Use
|
|
135
|
-
Stripe CLI during local testing to forward events to
|
|
136
|
-
`http://localhost:3014/api/billing/webhook`.
|
|
137
|
-
|
|
138
|
-
## Access surfaces
|
|
139
|
-
|
|
140
|
-
The `/api/v1` gateway is the product; every other surface is a thin client over
|
|
141
|
-
it. Point them at a running gateway with two env vars: `OTTOPORT_BASE_URL` and
|
|
142
|
-
`OTTOPORT_API_KEY` (the `op-…` key, or `OTTOPORT_KEY_SECRET` in dev).
|
|
143
|
-
|
|
144
|
-
- **HTTP** — OpenAI-compatible. Point any OpenAI SDK's `baseURL` at
|
|
145
|
-
`${OTTOPORT_BASE_URL}/api/v1`.
|
|
146
|
-
- **CLI** — [`cli/ottoport.mjs`](cli/ottoport.mjs), zero-dependency (Node 20+).
|
|
147
|
-
`npm run cli -- models` · `chat` · `image` · `video` · `speech` · `music`. Installs as the
|
|
148
|
-
`ottoport` bin. Streams chat, `--out` downloads image/video results.
|
|
149
|
-
- **MCP** — [`mcp/server.mjs`](mcp/server.mjs), a dependency-free stdio server
|
|
150
|
-
exposing `ottoport_list_models`, `ottoport_chat`, `ottoport_generate_image`,
|
|
151
|
-
`ottoport_generate_video`, `ottoport_generate_speech`, and `ottoport_generate_music`. Register with
|
|
152
|
-
`claude mcp add ottoport -- node $PWD/mcp/server.mjs`.
|
|
153
|
-
- **Skill** — [`skills/ottoport/SKILL.md`](skills/ottoport/SKILL.md) teaches an
|
|
154
|
-
agent to drive OttoPort via the CLI/MCP.
|
|
155
|
-
|
|
156
|
-
Shared TS client for embedding in your own code: [`libs/client.ts`](libs/client.ts).
|
|
157
|
-
|
|
158
|
-
## What's wired vs. stubbed
|
|
159
|
-
|
|
160
|
-
- ✅ Landing page, docs, playground, model catalog, unified gateway + adapters
|
|
161
|
-
- ✅ CLI, MCP server, and Skill — all thin clients over `/api/v1`
|
|
162
|
-
- ✅ Supabase sign-in requirement + metered landing-page playground (schema in `sql/schema.sql`)
|
|
163
|
-
- ✅ Stripe Checkout top-ups with verified, idempotent webhook crediting
|
|
164
|
-
- ✅ Hashed `api_keys` lookup in `libs/gateway/auth.ts` for the public `/api/v1` gateway
|
|
165
|
-
- 🔲 Dashboard key management UI
|
|
166
|
-
|
|
167
|
-
See the SQL migrations for the accounts / workspaces (projects) / api_keys / usage_events tables and RLS.
|
|
53
|
+
## Notes
|
|
54
|
+
|
|
55
|
+
- Image, video, and audio calls return **URLs**, and provider URLs expire.
|
|
56
|
+
Download anything worth keeping.
|
|
57
|
+
- Video generation takes a minute or more and the call blocks until the job is
|
|
58
|
+
terminal. A retry is a second billable generation, not a resumption.
|
|
59
|
+
- Requests are billed against the prepaid balance on your OttoPort account.
|
|
60
|
+
|
|
61
|
+
Docs: <https://ottoport.ai/docs> · Support: support@ottoport.ai
|
|
62
|
+
|
|
63
|
+
MIT © LITBOX LLC
|
package/cli/install.mjs
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// ── ottoport install <host> ──────────────────────────────────────────
|
|
2
|
+
// Wires OttoPort into whatever agent the user actually runs. Only Claude Code
|
|
3
|
+
// has a plugin format, so everywhere else the installable unit is the MCP
|
|
4
|
+
// server plus — where the host understands one — the skill.
|
|
5
|
+
//
|
|
6
|
+
// The server is always registered as `npx -y ottoport mcp`, so the host
|
|
7
|
+
// resolves it from npm rather than from a path that only exists on this
|
|
8
|
+
// machine.
|
|
9
|
+
|
|
10
|
+
import { spawnSync } from "node:child_process";
|
|
11
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync } from "node:fs";
|
|
12
|
+
import { homedir, platform } from "node:os";
|
|
13
|
+
import { dirname, join } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
|
|
16
|
+
const PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
17
|
+
const SKILL_SRC = join(PKG_ROOT, "skills/ottoport");
|
|
18
|
+
const HOME = homedir();
|
|
19
|
+
const CODEX_HOME = process.env.CODEX_HOME || join(HOME, ".codex");
|
|
20
|
+
|
|
21
|
+
const COMMAND = "npx";
|
|
22
|
+
const ARGS = ["-y", "ottoport", "mcp"];
|
|
23
|
+
// Names forwarded from the user's environment. Writing the key itself into a
|
|
24
|
+
// config file would leave an op-… secret in plain text on disk, and every host
|
|
25
|
+
// below can read it from the environment instead.
|
|
26
|
+
const ENV_NAMES = ["OTTOPORT_API_KEY", "OTTOPORT_BASE_URL"];
|
|
27
|
+
|
|
28
|
+
let DRY = false;
|
|
29
|
+
const done = [];
|
|
30
|
+
const notes = [];
|
|
31
|
+
|
|
32
|
+
function write(path, content) {
|
|
33
|
+
if (DRY) return void done.push(`would write ${path}`);
|
|
34
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
35
|
+
if (existsSync(path)) copyFileSync(path, `${path}.bak`); // never edit a config without a copy beside it
|
|
36
|
+
writeFileSync(path, content);
|
|
37
|
+
done.push(`wrote ${path}${existsSync(`${path}.bak`) ? " (backup: .bak)" : ""}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function installSkill(dir) {
|
|
41
|
+
const dest = join(dir, "ottoport");
|
|
42
|
+
if (!existsSync(SKILL_SRC)) {
|
|
43
|
+
notes.push(`skill not found in this package (${SKILL_SRC}) — install ottoport@1.3.0 or newer`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (DRY) return void done.push(`would install skill → ${dest}`);
|
|
47
|
+
mkdirSync(dir, { recursive: true });
|
|
48
|
+
cpSync(SKILL_SRC, dest, { recursive: true });
|
|
49
|
+
done.push(`installed skill → ${dest}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function hostCli(bin, args) {
|
|
53
|
+
const probe = spawnSync(bin, ["--version"], { stdio: "ignore" });
|
|
54
|
+
if (probe.error) return false;
|
|
55
|
+
if (DRY) {
|
|
56
|
+
done.push(`would run: ${bin} ${args.join(" ")}`);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
const run = spawnSync(bin, args, { stdio: "inherit" });
|
|
60
|
+
if (run.status !== 0) throw new Error(`${bin} ${args[0]} ${args[1]} exited ${run.status}`);
|
|
61
|
+
done.push(`registered with ${bin}`);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── hosts ────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
// Codex keeps MCP servers in TOML. `env_vars` forwards the named variables
|
|
68
|
+
// from the environment; the sibling `[...env]` table would hard-code values.
|
|
69
|
+
function codex() {
|
|
70
|
+
const path = join(CODEX_HOME, "config.toml");
|
|
71
|
+
const block =
|
|
72
|
+
`[mcp_servers.ottoport]\n` +
|
|
73
|
+
`command = "${COMMAND}"\n` +
|
|
74
|
+
`args = [${ARGS.map((a) => `"${a}"`).join(", ")}]\n` +
|
|
75
|
+
`env_vars = [${ENV_NAMES.map((n) => `"${n}"`).join(", ")}]\n`;
|
|
76
|
+
|
|
77
|
+
const current = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
78
|
+
// Replace an existing entry in place — from its header to the next
|
|
79
|
+
// top-of-line table header — so re-running is idempotent rather than additive.
|
|
80
|
+
const existing = /^\[mcp_servers\.ottoport\][^\n]*\n(?:(?!^\[)[^\n]*\n?)*/m;
|
|
81
|
+
const next = existing.test(current)
|
|
82
|
+
? current.replace(existing, block)
|
|
83
|
+
: (current.trimEnd() + (current.trim() ? "\n\n" : "") + block);
|
|
84
|
+
write(path, next);
|
|
85
|
+
installSkill(join(CODEX_HOME, "skills"));
|
|
86
|
+
notes.push("restart Codex to pick up the server and skill");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Claude Code has a real plugin (commands included), so the MCP-only path here
|
|
90
|
+
// is the fallback for someone who would rather not add the marketplace.
|
|
91
|
+
function claudeCode() {
|
|
92
|
+
if (!hostCli("claude", ["mcp", "add", "ottoport", "--", COMMAND, ...ARGS])) {
|
|
93
|
+
notes.push("claude CLI not found — run: claude mcp add ottoport -- npx -y ottoport mcp");
|
|
94
|
+
}
|
|
95
|
+
installSkill(join(HOME, ".claude/skills"));
|
|
96
|
+
notes.push(
|
|
97
|
+
"for the full plugin (slash commands too):\n" +
|
|
98
|
+
" /plugin marketplace add https://ottoport.ai/plugin/marketplace.json\n" +
|
|
99
|
+
" /plugin install ottoport@ottoport",
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Hermes stores MCP servers in YAML. Editing that by hand from here would risk
|
|
104
|
+
// mangling a user's config, and Hermes ships a command for exactly this.
|
|
105
|
+
function hermes() {
|
|
106
|
+
// `--args` takes the rest of the line, so it has to come last.
|
|
107
|
+
if (!hostCli("hermes", ["mcp", "add", "ottoport", "--command", COMMAND, "--args", ...ARGS])) {
|
|
108
|
+
notes.push(
|
|
109
|
+
"hermes CLI not found. Add to ~/.hermes/config.yaml:\n" +
|
|
110
|
+
" mcp_servers:\n ottoport:\n command: npx\n args: [-y, ottoport, mcp]",
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
notes.push("Hermes has no skill mechanism — the MCP tools carry their own descriptions");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Cursor, Windsurf and Claude Desktop all read a JSON file with the same shape.
|
|
117
|
+
function jsonHost(label, path) {
|
|
118
|
+
const config = existsSync(path) ? JSON.parse(readFileSync(path, "utf8")) : {};
|
|
119
|
+
config.mcpServers = {
|
|
120
|
+
...config.mcpServers,
|
|
121
|
+
ottoport: { command: COMMAND, args: ARGS },
|
|
122
|
+
};
|
|
123
|
+
write(path, JSON.stringify(config, null, 2) + "\n");
|
|
124
|
+
notes.push(
|
|
125
|
+
`${label} launches the server from your desktop session, which may not see a key exported in a shell. ` +
|
|
126
|
+
`If the tools report 401, add an "env" block to the ottoport entry.`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const HOSTS = {
|
|
131
|
+
codex,
|
|
132
|
+
claude: claudeCode,
|
|
133
|
+
"claude-code": claudeCode,
|
|
134
|
+
hermes,
|
|
135
|
+
cursor: () => jsonHost("Cursor", join(HOME, ".cursor/mcp.json")),
|
|
136
|
+
windsurf: () => jsonHost("Windsurf", join(HOME, ".codeium/windsurf/mcp_config.json")),
|
|
137
|
+
"claude-desktop": () =>
|
|
138
|
+
jsonHost(
|
|
139
|
+
"Claude Desktop",
|
|
140
|
+
platform() === "darwin"
|
|
141
|
+
? join(HOME, "Library/Application Support/Claude/claude_desktop_config.json")
|
|
142
|
+
: join(HOME, ".config/Claude/claude_desktop_config.json"),
|
|
143
|
+
),
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export function installHosts() {
|
|
147
|
+
return Object.keys(HOSTS);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function cmdInstall(host, flags) {
|
|
151
|
+
DRY = flags["dry-run"] === true || flags.n === true;
|
|
152
|
+
const run = HOSTS[host];
|
|
153
|
+
if (!run) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`unknown host "${host ?? ""}". Try one of: ${installHosts().join(", ")}`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
run();
|
|
160
|
+
|
|
161
|
+
process.stdout.write(done.map((d) => ` ${d}\n`).join("") || " nothing to do\n");
|
|
162
|
+
if (!process.env.OTTOPORT_API_KEY && !process.env.OTTOPORT_KEY_SECRET) {
|
|
163
|
+
notes.unshift(
|
|
164
|
+
"OTTOPORT_API_KEY is not set in this environment. Export it from your shell profile " +
|
|
165
|
+
"(create a key at https://ottoport.ai/api-keys) — the server reads it at launch.",
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
if (notes.length) process.stdout.write("\n" + notes.map((n) => `note: ${n}\n`).join(""));
|
|
169
|
+
}
|
package/cli/ottoport.mjs
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// ottoport speech "text" [--model gpt-4o-mini-tts] [--voice alloy] [--out speech.mp3]
|
|
11
11
|
// ottoport music "prompt" [--model suno-v5] [--duration 15] [--out song.mp3]
|
|
12
12
|
// ottoport mcp
|
|
13
|
+
// ottoport install <codex|claude|hermes|cursor|windsurf|claude-desktop>
|
|
13
14
|
//
|
|
14
15
|
// Config (flags override env):
|
|
15
16
|
// OTTOPORT_API_KEY your op-… key (or the dev OTTOPORT_KEY_SECRET)
|
|
@@ -251,6 +252,9 @@ Usage:
|
|
|
251
252
|
ottoport speech "<text>" [--model gpt-4o-mini-tts] [--voice alloy] [--format mp3] [--out speech.mp3]
|
|
252
253
|
ottoport music "<prompt>" [--model suno-v5] [--duration 15] [--format mp3] [--out song.mp3]
|
|
253
254
|
ottoport mcp
|
|
255
|
+
ottoport install <host> wire the MCP server (and skill) into an agent
|
|
256
|
+
hosts: codex, claude, hermes, cursor, windsurf, claude-desktop
|
|
257
|
+
[--dry-run]
|
|
254
258
|
|
|
255
259
|
Global flags:
|
|
256
260
|
--url <base> gateway base URL (env OTTOPORT_BASE_URL)
|
|
@@ -265,6 +269,7 @@ Examples:
|
|
|
265
269
|
ottoport speech "Welcome to OttoPort" --voice alloy --out welcome.mp3
|
|
266
270
|
ottoport music "lo-fi focus beat" --duration 20 --out focus.mp3
|
|
267
271
|
ottoport mcp
|
|
272
|
+
ottoport install codex
|
|
268
273
|
`;
|
|
269
274
|
|
|
270
275
|
async function main() {
|
|
@@ -280,6 +285,10 @@ async function main() {
|
|
|
280
285
|
case "speech": return await cmdAudio("speech", prompt, flags);
|
|
281
286
|
case "music": return await cmdAudio("music", prompt, flags);
|
|
282
287
|
case "mcp": return await import(new URL("../mcp/server.mjs", import.meta.url));
|
|
288
|
+
case "install": {
|
|
289
|
+
const { cmdInstall } = await import(new URL("./install.mjs", import.meta.url));
|
|
290
|
+
return await cmdInstall(rest[0], flags);
|
|
291
|
+
}
|
|
283
292
|
case undefined:
|
|
284
293
|
case "help":
|
|
285
294
|
case "--help":
|
package/commands/chat.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Run a prompt through any chat model on the OttoPort gateway
|
|
3
|
+
argument-hint: "<prompt> [--model <id>]"
|
|
4
|
+
allowed-tools: mcp__ottoport__ottoport_chat, mcp__ottoport__ottoport_list_models
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Send this to a model through the `ottoport_chat` MCP tool:
|
|
8
|
+
|
|
9
|
+
$ARGUMENTS
|
|
10
|
+
|
|
11
|
+
If the request names a model, pass it as `model`; otherwise let the tool default
|
|
12
|
+
to `claude-sonnet-5`. If the named model is rejected as unknown, call
|
|
13
|
+
`ottoport_list_models` with `modality: "chat"`, pick the closest real id, and say
|
|
14
|
+
which one you substituted.
|
|
15
|
+
|
|
16
|
+
Return the model's answer itself — do not summarize it away.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Generate an image through the OttoPort gateway
|
|
3
|
+
argument-hint: "<prompt> [--model <id>] [--size 1024x1024]"
|
|
4
|
+
allowed-tools: mcp__ottoport__ottoport_generate_image, mcp__ottoport__ottoport_list_models, Bash(curl:*)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Generate an image with the `ottoport_generate_image` MCP tool.
|
|
8
|
+
|
|
9
|
+
Request: $ARGUMENTS
|
|
10
|
+
|
|
11
|
+
- Pass `model` only if the user named one; the tool defaults to `gpt-image-2`.
|
|
12
|
+
- An edit of an existing image goes through `image_url`, not the prompt.
|
|
13
|
+
- The tool returns URLs, and provider URLs expire. Print the URL, and if the
|
|
14
|
+
user asked for a file, download it to the path they named with `curl -sSL`.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: List the OttoPort model catalog with live pricing, optionally filtered by modality
|
|
3
|
+
argument-hint: "[chat|image|video|tts|music]"
|
|
4
|
+
allowed-tools: mcp__ottoport__ottoport_list_models
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
List the OttoPort catalog using the `ottoport_list_models` MCP tool.
|
|
8
|
+
|
|
9
|
+
Modality filter (may be empty — then list everything): `$1`
|
|
10
|
+
|
|
11
|
+
Present the result as a table: model id, what it is good at, and price. Group by
|
|
12
|
+
modality when no filter was given, and keep it to the models worth choosing
|
|
13
|
+
between rather than every row. Never invent a model id that the tool did not
|
|
14
|
+
return.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Check that the OttoPort plugin is wired up — key, gateway reachability, MCP tools
|
|
3
|
+
allowed-tools: Bash(node:*), Bash(printenv:*), mcp__ottoport__ottoport_list_models
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Verify this machine can reach OttoPort, then report what is and is not working.
|
|
7
|
+
|
|
8
|
+
1. Read the environment: `printenv OTTOPORT_API_KEY OTTOPORT_BASE_URL` (print
|
|
9
|
+
only whether the key is set and what prefix it has — never echo the key).
|
|
10
|
+
2. Call `ottoport_list_models`. Success means the MCP server, the base URL and
|
|
11
|
+
the key are all good; stop and report.
|
|
12
|
+
3. If that fails, the fix depends on the error:
|
|
13
|
+
- no `ottoport` MCP tools at all → the plugin's MCP server did not start;
|
|
14
|
+
check `node --version` is 20+.
|
|
15
|
+
- `401`/`invalid key` → the key is missing or wrong. Keys are created at
|
|
16
|
+
https://ottoport.ai/api-keys and look like `op-…`.
|
|
17
|
+
- connection refused → `OTTOPORT_BASE_URL` points at a gateway that is not
|
|
18
|
+
running. It defaults to `https://ottoport.ai`; a local dev gateway is
|
|
19
|
+
`http://localhost:3014`.
|
|
20
|
+
|
|
21
|
+
Both variables belong in the user's shell profile, since the MCP server reads
|
|
22
|
+
them from the environment Claude Code was launched in.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Turn text into speech, or a prompt into music, through the OttoPort gateway
|
|
3
|
+
argument-hint: "<text> [--voice alloy] [--music]"
|
|
4
|
+
allowed-tools: mcp__ottoport__ottoport_generate_speech, mcp__ottoport__ottoport_generate_music, Bash(curl:*)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Request: $ARGUMENTS
|
|
8
|
+
|
|
9
|
+
Use `ottoport_generate_music` when the request is for music, a song, or a
|
|
10
|
+
backing track; otherwise use `ottoport_generate_speech` to read the text aloud.
|
|
11
|
+
Pass `voice` and `format` only when named — speech defaults to `gpt-4o-mini-tts`
|
|
12
|
+
with the `alloy` voice, music to `suno-v5`.
|
|
13
|
+
|
|
14
|
+
Print the returned audio URL, and save it to disk with `curl -sSL` only if the
|
|
15
|
+
user asked for a file.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Generate a video through the OttoPort gateway
|
|
3
|
+
argument-hint: "<prompt> [--model <id>] [--duration 5]"
|
|
4
|
+
allowed-tools: mcp__ottoport__ottoport_generate_video, mcp__ottoport__ottoport_list_models, Bash(curl:*)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Generate a video with the `ottoport_generate_video` MCP tool.
|
|
8
|
+
|
|
9
|
+
Request: $ARGUMENTS
|
|
10
|
+
|
|
11
|
+
- Pass `model`, `duration`, `aspect_ratio` and `image_url` only when the request
|
|
12
|
+
supplies them; the tool defaults to `kling-3.0`.
|
|
13
|
+
- Video generation takes a minute or more and the call blocks until the job is
|
|
14
|
+
terminal. Say so before you start, and do not retry a call that is merely slow
|
|
15
|
+
— a retry is a second billable generation.
|
|
16
|
+
- Print the returned URL, and download it with `curl -sSL` only if the user asked
|
|
17
|
+
for a file on disk.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ottoport",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "CLI and MCP server for OttoPort — one OpenAI-compatible API for every LLM, image, video, and speech model.",
|
|
3
|
+
"version": "1.3.0",
|
|
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",
|
|
7
7
|
"keywords": [
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
"text-to-speech",
|
|
15
15
|
"mcp",
|
|
16
16
|
"mcp-server",
|
|
17
|
-
"cli"
|
|
17
|
+
"cli",
|
|
18
|
+
"claude-code",
|
|
19
|
+
"claude-code-plugin"
|
|
18
20
|
],
|
|
19
21
|
"engines": {
|
|
20
22
|
"node": ">=20.0.0"
|
|
@@ -22,44 +24,8 @@
|
|
|
22
24
|
"bin": {
|
|
23
25
|
"ottoport": "cli/ottoport.mjs"
|
|
24
26
|
},
|
|
25
|
-
"files": [
|
|
26
|
-
"cli",
|
|
27
|
-
"mcp",
|
|
28
|
-
"README.md"
|
|
29
|
-
],
|
|
30
27
|
"scripts": {
|
|
31
|
-
"dev": "next dev -p 3014",
|
|
32
|
-
"build": "next build",
|
|
33
|
-
"start": "next start -p 3014",
|
|
34
|
-
"lint": "next lint",
|
|
35
28
|
"cli": "node cli/ottoport.mjs",
|
|
36
29
|
"mcp": "node mcp/server.mjs"
|
|
37
|
-
},
|
|
38
|
-
"devDependencies": {
|
|
39
|
-
"@anthropic-ai/sdk": "^0.65.0",
|
|
40
|
-
"@supabase/ssr": "^0.10.2",
|
|
41
|
-
"@supabase/supabase-js": "^2.43.0",
|
|
42
|
-
"@tailwindcss/typography": "^0.5.13",
|
|
43
|
-
"@types/node": "^20.14.0",
|
|
44
|
-
"@types/react": "^18.3.3",
|
|
45
|
-
"@types/react-dom": "^18.3.0",
|
|
46
|
-
"autoprefixer": "^10.4.19",
|
|
47
|
-
"clsx": "^2.1.1",
|
|
48
|
-
"daisyui": "^4.12.0",
|
|
49
|
-
"eslint": "^8.57.0",
|
|
50
|
-
"eslint-config-next": "^14.2.0",
|
|
51
|
-
"lucide-react": "^0.395.0",
|
|
52
|
-
"next": "^14.2.0",
|
|
53
|
-
"openai": "^4.104.0",
|
|
54
|
-
"postcss": "^8.4.38",
|
|
55
|
-
"react": "^18.3.0",
|
|
56
|
-
"react-dom": "^18.3.0",
|
|
57
|
-
"sonner": "^1.5.0",
|
|
58
|
-
"stripe": "^13.11.0",
|
|
59
|
-
"swr": "^2.2.5",
|
|
60
|
-
"tailwind-merge": "^2.3.0",
|
|
61
|
-
"tailwindcss": "^3.4.4",
|
|
62
|
-
"typescript": "^5.4.5",
|
|
63
|
-
"zod": "^3.23.8"
|
|
64
30
|
}
|
|
65
31
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ottoport
|
|
3
|
+
description: Generate text, images, and video through OttoPort — one unified, OpenAI-compatible API for every model (GPT, Claude, FLUX, Nano Banana, Kling, Veo 3). Use when the user wants to call an LLM, generate an image, or generate a video and has OttoPort configured, or asks to "use OttoPort".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# OttoPort
|
|
7
|
+
|
|
8
|
+
OttoPort is a single OpenAI-compatible gateway that routes to first-party model
|
|
9
|
+
providers. One key, one base URL, three modalities: **chat**, **image**, **video**.
|
|
10
|
+
|
|
11
|
+
## Setup
|
|
12
|
+
|
|
13
|
+
Two environment variables drive every access path (CLI, MCP, raw HTTP):
|
|
14
|
+
|
|
15
|
+
- `OTTOPORT_API_KEY` — an `op-…` key (or the dev `OTTOPORT_KEY_SECRET`).
|
|
16
|
+
- `OTTOPORT_BASE_URL` — e.g. `https://ottoport.dev` or `http://localhost:3014`.
|
|
17
|
+
|
|
18
|
+
Check what's available before generating:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
node cli/ottoport.mjs models # full catalog with pricing
|
|
22
|
+
node cli/ottoport.mjs models --modality image
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Choosing a model
|
|
26
|
+
|
|
27
|
+
| Modality | Good default | Also available |
|
|
28
|
+
| --- | --- | --- |
|
|
29
|
+
| chat | `claude-sonnet-5` | `claude-haiku-4.5` (cheap), `gpt-5.5`, `gemini-3.5-flash` |
|
|
30
|
+
| image | `gpt-image-2` | `nano-banana-pro` (high fidelity), `nano-banana-2` (edits) |
|
|
31
|
+
| video | `kling-3.0` | `seedance-2.0-fast` (cheap), `veo-3.1` (with audio) |
|
|
32
|
+
| tts | `gpt-4o-mini-tts` | `eleven-v3` |
|
|
33
|
+
| music | `suno-v5` | `lyria-2` |
|
|
34
|
+
|
|
35
|
+
Never invent model ids — run `ottoport models` if unsure.
|
|
36
|
+
|
|
37
|
+
## CLI (preferred for one-off calls)
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Chat (streams by default)
|
|
41
|
+
node cli/ottoport.mjs chat "explain MCP in one sentence" --model claude-haiku-4.5
|
|
42
|
+
node cli/ottoport.mjs chat "summarize this" --system "You are terse." --no-stream
|
|
43
|
+
|
|
44
|
+
# Image → prints URL(s); --out downloads the first
|
|
45
|
+
node cli/ottoport.mjs image "isometric city at dusk" --model nano-banana-pro --out city.png
|
|
46
|
+
|
|
47
|
+
# Video → blocks on the queue, prints the URL
|
|
48
|
+
node cli/ottoport.mjs video "drone shot over a canyon" --duration 5 --out clip.mp4
|
|
49
|
+
|
|
50
|
+
# Speech and music → print an audio URL; --out downloads it
|
|
51
|
+
node cli/ottoport.mjs speech "Welcome to OttoPort" --voice alloy --out welcome.mp3
|
|
52
|
+
node cli/ottoport.mjs music "lo-fi focus beat" --duration 20 --out focus.mp3
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## HTTP (for scripts / other languages)
|
|
56
|
+
|
|
57
|
+
The gateway is OpenAI-compatible, so existing SDKs work by pointing `baseURL`
|
|
58
|
+
at `${OTTOPORT_BASE_URL}/api/v1` with the OttoPort key.
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
curl $OTTOPORT_BASE_URL/api/v1/chat/completions \
|
|
62
|
+
-H "Authorization: Bearer $OTTOPORT_API_KEY" -H "Content-Type: application/json" \
|
|
63
|
+
-d '{"model":"gpt-4o","messages":[{"role":"user","content":"hi"}]}'
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Image: `POST /api/v1/images/generations` `{model, prompt, size?, n?, image_url?}` → `{data:[{url}]}`.
|
|
67
|
+
Video: `POST /api/v1/videos/generations` `{model, prompt, duration?, aspect_ratio?, image_url?}` → a job with `data:[{url}]`.
|
|
68
|
+
|
|
69
|
+
## MCP (for agent tool use inside another client)
|
|
70
|
+
|
|
71
|
+
If OttoPort's MCP server is registered, prefer its tools over shelling out:
|
|
72
|
+
`ottoport_list_models`, `ottoport_chat`, `ottoport_generate_image`,
|
|
73
|
+
`ottoport_generate_video`, `ottoport_generate_speech`, `ottoport_generate_music`.
|
|
74
|
+
Register it with:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
claude mcp add ottoport -- node /abs/path/to/mcp/server.mjs
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Plugin (Claude Code)
|
|
81
|
+
|
|
82
|
+
Installing the OttoPort plugin brings the MCP server, this skill, and the
|
|
83
|
+
`/ottoport:*` slash commands in one step — no manual `claude mcp add`:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
/plugin marketplace add https://ottoport.ai/plugin/marketplace.json
|
|
87
|
+
/plugin install ottoport@ottoport
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Commands: `/ottoport:models`, `/ottoport:chat`, `/ottoport:image`,
|
|
91
|
+
`/ottoport:video`, `/ottoport:speech`, `/ottoport:setup`.
|
|
92
|
+
|
|
93
|
+
## Tips
|
|
94
|
+
|
|
95
|
+
- Image and video calls return **URLs**, not files — download with `--out` (CLI)
|
|
96
|
+
or fetch the URL yourself. Provider URLs may be short-lived; persist anything
|
|
97
|
+
you need to keep.
|
|
98
|
+
- Video generation can take a minute; it blocks server-side and returns when done.
|
|
99
|
+
- Errors come back as `{error:{message, code}}`; surface the message to the user.
|