@flatkey-ai/cli 0.1.5 → 0.1.6
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/README.md +27 -1
- package/package.json +5 -1
- package/src/api.js +20 -1
- package/src/cli.js +43 -1
- package/src/config.js +3 -3
- package/src/help.js +144 -3
- package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -50
- package/.github/ISSUE_TEMPLATE/feature_request.yml +0 -34
- package/.github/pull_request_template.md +0 -13
- package/.github/workflows/npm-publish.yml +0 -36
- package/CONTRIBUTING.md +0 -37
- package/SECURITY.md +0 -13
- package/docs/superpowers/plans/2026-07-17-flatkey-cli.md +0 -90
- package/docs/superpowers/specs/2026-07-17-flatkey-cli-design.md +0 -209
- package/flatkey-output/audio-01.mp3 +0 -1
- package/release/deb/README.md +0 -16
- package/release/homebrew/flatkey.rb +0 -16
- package/release/msi/README.md +0 -15
- package/test/animation.test.js +0 -30
- package/test/api.test.js +0 -282
- package/test/artifacts.test.js +0 -108
- package/test/cli.test.js +0 -143
- package/test/config.test.js +0 -62
- package/test/help.test.js +0 -33
- package/test/integration.test.js +0 -274
- package/test/live-flatkey.test.js +0 -121
- package/test/models.test.js +0 -56
- package/test/package.test.js +0 -29
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
# Flatkey CLI Design
|
|
2
|
-
|
|
3
|
-
## Goal
|
|
4
|
-
|
|
5
|
-
Build `@flatkey-ai/cli`, a cross-platform npm CLI for media workers and AI agents to use Flatkey as the most cost-effective practical AI API entrypoint for image, video, and audio generation. The CLI should make cost, credits, model choice, upload inputs, generation jobs, and saved artifacts visible from the terminal.
|
|
6
|
-
|
|
7
|
-
## Scope
|
|
8
|
-
|
|
9
|
-
The CLI provides:
|
|
10
|
-
|
|
11
|
-
- Image generation.
|
|
12
|
-
- Video generation.
|
|
13
|
-
- Audio generation.
|
|
14
|
-
- Cost estimation before generation.
|
|
15
|
-
- Generation job creation, listing, inspection, and waiting.
|
|
16
|
-
- Uploads for local image, video, and audio inputs.
|
|
17
|
-
- Credit balance and usage status.
|
|
18
|
-
- Recent credit transactions.
|
|
19
|
-
- AI-first help output.
|
|
20
|
-
- Available model listing.
|
|
21
|
-
- Model parameter inspection.
|
|
22
|
-
- Local onboarding for a Flatkey API key.
|
|
23
|
-
- A short ASCII animation during human-facing generation commands.
|
|
24
|
-
|
|
25
|
-
The first implementation is a Node.js npm package. It runs on Windows, Linux, and macOS through Node. Homebrew, apt/deb, and MSI packaging are release channels around the npm package, not separate native implementations.
|
|
26
|
-
|
|
27
|
-
## Package and Binary
|
|
28
|
-
|
|
29
|
-
Package name: `@flatkey-ai/cli`.
|
|
30
|
-
|
|
31
|
-
Primary binary: `flatkey`.
|
|
32
|
-
|
|
33
|
-
The package uses ESM JavaScript and keeps runtime dependencies minimal. It should run with the current active Node LTS line and newer.
|
|
34
|
-
|
|
35
|
-
## Command Surface
|
|
36
|
-
|
|
37
|
-
Top-level commands:
|
|
38
|
-
|
|
39
|
-
- `flatkey onboard [--api-key <key>]`
|
|
40
|
-
- `flatkey image generate --prompt "<prompt>" [options]`
|
|
41
|
-
- `flatkey video generate --prompt "<prompt>" [options]`
|
|
42
|
-
- `flatkey audio generate --prompt "<prompt>" [options]`
|
|
43
|
-
- `flatkey generate create <model> --prompt "<prompt>" [options]`
|
|
44
|
-
- `flatkey generate cost <model> --prompt "<prompt>" [options]`
|
|
45
|
-
- `flatkey generate get <job-id> [--json]`
|
|
46
|
-
- `flatkey generate list [--type image|video|audio] [--json]`
|
|
47
|
-
- `flatkey generate wait <job-id> [--json]`
|
|
48
|
-
- `flatkey upload create <file> [--json]`
|
|
49
|
-
- `flatkey upload list [--type image|video|audio] [--json]`
|
|
50
|
-
- `flatkey credits [--json]`
|
|
51
|
-
- `flatkey status [--json]`
|
|
52
|
-
- `flatkey transactions [--size <count>] [--json]`
|
|
53
|
-
- `flatkey models [--type image|video|audio] [--json]`
|
|
54
|
-
- `flatkey models get <model> [--json]`
|
|
55
|
-
- `flatkey help [--ai] [--json]`
|
|
56
|
-
- `flatkey version`
|
|
57
|
-
|
|
58
|
-
Shared generation options:
|
|
59
|
-
|
|
60
|
-
- `--model <model>`
|
|
61
|
-
- `--base-url <url>`
|
|
62
|
-
- `--api-key <key>`
|
|
63
|
-
- `--out <dir>`
|
|
64
|
-
- `--json`
|
|
65
|
-
|
|
66
|
-
Image-specific options:
|
|
67
|
-
|
|
68
|
-
- `--size <width>x<height>`
|
|
69
|
-
- `--aspect <ratio>`
|
|
70
|
-
- `--n <count>`
|
|
71
|
-
- `--quality <quality>`
|
|
72
|
-
|
|
73
|
-
Video-specific options:
|
|
74
|
-
|
|
75
|
-
- `--duration <seconds>`
|
|
76
|
-
- `--aspect <ratio>`
|
|
77
|
-
- `--fps <number>`
|
|
78
|
-
|
|
79
|
-
Audio-specific options:
|
|
80
|
-
|
|
81
|
-
- `--voice <voice>`
|
|
82
|
-
- `--format <mp3|wav|aac|flac>`
|
|
83
|
-
|
|
84
|
-
## Authentication and Config
|
|
85
|
-
|
|
86
|
-
Credential resolution order:
|
|
87
|
-
|
|
88
|
-
1. `--api-key`
|
|
89
|
-
2. `FLATKEY_API_KEY`
|
|
90
|
-
3. Saved config at `~/.config/flatkey/config.json`
|
|
91
|
-
|
|
92
|
-
`flatkey onboard` writes the config file with mode `0600` where supported. Windows should ignore chmod failures.
|
|
93
|
-
|
|
94
|
-
## Flatkey API Integration
|
|
95
|
-
|
|
96
|
-
Default base URL: `https://router.flatkey.ai`.
|
|
97
|
-
|
|
98
|
-
Image generation follows the proven `awesome-images` behavior:
|
|
99
|
-
|
|
100
|
-
- OpenAI-compatible image endpoint: `POST /v1/images/generations`
|
|
101
|
-
- Gemini-style Nano endpoint: `POST /v1beta/models/{model}:generateContent?key={apiKey}`
|
|
102
|
-
|
|
103
|
-
The CLI should default image generation to the Nano route when `--model` is omitted or starts with `nano`, and use `/v1/images/generations` for `gpt` or `gpt-image-*`.
|
|
104
|
-
|
|
105
|
-
Video, audio, cost, jobs, uploads, credits, status, transactions, and models are implemented through small provider functions with configurable endpoint paths. The initial default paths are:
|
|
106
|
-
|
|
107
|
-
- `POST /v1/videos/generations`
|
|
108
|
-
- `POST /v1/audio/generations`
|
|
109
|
-
- `POST /v1/generations`
|
|
110
|
-
- `POST /v1/generations/cost`
|
|
111
|
-
- `GET /v1/generations`
|
|
112
|
-
- `GET /v1/generations/{jobId}`
|
|
113
|
-
- `POST /v1/uploads`
|
|
114
|
-
- `GET /v1/uploads`
|
|
115
|
-
- `GET /v1/credits`
|
|
116
|
-
- `GET /v1/credits/transactions`
|
|
117
|
-
- `GET /v1/status`
|
|
118
|
-
- `GET /v1/models`
|
|
119
|
-
- `GET /v1/models/{model}`
|
|
120
|
-
|
|
121
|
-
If Flatkey router returns a different shape, adapters normalize results before printing or saving artifacts.
|
|
122
|
-
|
|
123
|
-
## Output and Artifacts
|
|
124
|
-
|
|
125
|
-
Default output directory: `flatkey-output`.
|
|
126
|
-
|
|
127
|
-
Generation commands save returned files when the API returns base64 or data URLs, and print remote URLs when the API returns URLs. JSON mode prints a machine-readable payload and suppresses animation/logging.
|
|
128
|
-
|
|
129
|
-
Artifact naming:
|
|
130
|
-
|
|
131
|
-
- `image-01.png`
|
|
132
|
-
- `video-01.mp4`
|
|
133
|
-
- `audio-01.mp3`
|
|
134
|
-
|
|
135
|
-
Extensions come from returned MIME types when available.
|
|
136
|
-
|
|
137
|
-
## AI-First Help
|
|
138
|
-
|
|
139
|
-
`flatkey help --ai` prints a compact protocol-style guide:
|
|
140
|
-
|
|
141
|
-
- Required setup.
|
|
142
|
-
- Commands.
|
|
143
|
-
- Arguments.
|
|
144
|
-
- Environment variables.
|
|
145
|
-
- JSON mode contract.
|
|
146
|
-
- Example calls for image, video, audio, cost, upload, job wait, credits, transactions, status, and models.
|
|
147
|
-
- Error recovery instructions for missing key, unknown model, and API failure.
|
|
148
|
-
|
|
149
|
-
Human `--help` remains readable but secondary.
|
|
150
|
-
|
|
151
|
-
## Models
|
|
152
|
-
|
|
153
|
-
`flatkey models` fetches remote model metadata when API key and network are available. If remote fetch fails, it falls back to bundled defaults:
|
|
154
|
-
|
|
155
|
-
- Image: `nano-banana-pro-preview`, `nano-banana-flash`, `gpt-image-2`
|
|
156
|
-
- Video: `veo-3`, `veo-3-fast`
|
|
157
|
-
- Audio: `tts-1`, `gpt-4o-mini-tts`
|
|
158
|
-
|
|
159
|
-
The fallback must mark `source: "bundled"` in JSON output.
|
|
160
|
-
|
|
161
|
-
## ASCII Animation
|
|
162
|
-
|
|
163
|
-
Human generation mode shows a short stderr animation before and during network calls. Requirements:
|
|
164
|
-
|
|
165
|
-
- Uses plain ASCII only.
|
|
166
|
-
- No animation in `--json`.
|
|
167
|
-
- Does not corrupt stdout artifact paths.
|
|
168
|
-
- Works in Windows terminals.
|
|
169
|
-
- Automatically falls back to simple log lines when stderr is not a TTY.
|
|
170
|
-
|
|
171
|
-
## Error Handling
|
|
172
|
-
|
|
173
|
-
Errors should be short and actionable:
|
|
174
|
-
|
|
175
|
-
- Missing key: explain `flatkey onboard` and `FLATKEY_API_KEY`.
|
|
176
|
-
- Unknown command: point to `flatkey help --ai`.
|
|
177
|
-
- HTTP failure: show API message when present, otherwise status code.
|
|
178
|
-
- Save failure: show output path and filesystem error.
|
|
179
|
-
|
|
180
|
-
JSON mode errors still exit non-zero and write error JSON to stderr.
|
|
181
|
-
|
|
182
|
-
## Testing
|
|
183
|
-
|
|
184
|
-
Use Node's built-in `node:test` and `assert/strict`.
|
|
185
|
-
|
|
186
|
-
Test coverage:
|
|
187
|
-
|
|
188
|
-
- Argument parsing.
|
|
189
|
-
- Config read/write and env precedence.
|
|
190
|
-
- Request building for image OpenAI and Nano routes.
|
|
191
|
-
- Request building for video/audio/credits/status/models.
|
|
192
|
-
- Artifact persistence from base64, data URL, and URL payloads.
|
|
193
|
-
- JSON mode suppresses animation/logging.
|
|
194
|
-
- CLI subprocess tests against local HTTP servers.
|
|
195
|
-
|
|
196
|
-
## Release Channels
|
|
197
|
-
|
|
198
|
-
Primary release is npm:
|
|
199
|
-
|
|
200
|
-
- `npm publish --access public`
|
|
201
|
-
- Users run `npx @flatkey-ai/cli` or install globally.
|
|
202
|
-
|
|
203
|
-
Secondary release assets:
|
|
204
|
-
|
|
205
|
-
- Homebrew formula that installs the npm package and exposes `flatkey`.
|
|
206
|
-
- Debian package wrapping the npm package and generated launcher.
|
|
207
|
-
- MSI package wrapping the npm package and generated Windows launcher.
|
|
208
|
-
|
|
209
|
-
Release automation should be added after the CLI is working and tested.
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
audio-file
|
package/release/deb/README.md
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
# Debian Package
|
|
2
|
-
|
|
3
|
-
Build `.deb` as a wrapper around the published npm package.
|
|
4
|
-
|
|
5
|
-
Expected install behavior:
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
sudo apt-get install flatkey
|
|
9
|
-
flatkey help --ai
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
Package contents:
|
|
13
|
-
|
|
14
|
-
- Node runtime dependency.
|
|
15
|
-
- Global install of `@flatkey-ai/cli`.
|
|
16
|
-
- `/usr/bin/flatkey` launcher.
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
class Flatkey < Formula
|
|
2
|
-
desc "Flatkey media generation CLI"
|
|
3
|
-
homepage "https://github.com/flatkey-ai/flatkey-cli"
|
|
4
|
-
license "MIT"
|
|
5
|
-
|
|
6
|
-
depends_on "node"
|
|
7
|
-
|
|
8
|
-
def install
|
|
9
|
-
system "npm", "install", "-g", "@flatkey-ai/cli", "--prefix", libexec
|
|
10
|
-
bin.install_symlink Dir["#{libexec}/bin/flatkey"].first
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
test do
|
|
14
|
-
system "#{bin}/flatkey", "help", "--ai"
|
|
15
|
-
end
|
|
16
|
-
end
|
package/release/msi/README.md
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
# Windows MSI
|
|
2
|
-
|
|
3
|
-
Build MSI as a wrapper around the published npm package.
|
|
4
|
-
|
|
5
|
-
Expected install behavior:
|
|
6
|
-
|
|
7
|
-
```powershell
|
|
8
|
-
flatkey help --ai
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
Package contents:
|
|
12
|
-
|
|
13
|
-
- Node runtime prerequisite or bundled runtime.
|
|
14
|
-
- Install of `@flatkey-ai/cli`.
|
|
15
|
-
- `flatkey.cmd` on PATH.
|
package/test/animation.test.js
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { test } from "node:test";
|
|
3
|
-
|
|
4
|
-
import { createAnimation } from "../src/animation.js";
|
|
5
|
-
|
|
6
|
-
test("does not animate in json mode", () => {
|
|
7
|
-
let output = "";
|
|
8
|
-
const animation = createAnimation({
|
|
9
|
-
json: true,
|
|
10
|
-
stream: { isTTY: true, write: (chunk) => { output += chunk; } },
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
animation.start("image");
|
|
14
|
-
animation.stop();
|
|
15
|
-
|
|
16
|
-
assert.equal(output, "");
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
test("uses single log line when stderr is not a tty", () => {
|
|
20
|
-
let output = "";
|
|
21
|
-
const animation = createAnimation({
|
|
22
|
-
json: false,
|
|
23
|
-
stream: { isTTY: false, write: (chunk) => { output += chunk; } },
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
animation.start("video");
|
|
27
|
-
animation.stop();
|
|
28
|
-
|
|
29
|
-
assert.equal(output, "flatkey video generation started\n");
|
|
30
|
-
});
|
package/test/api.test.js
DELETED
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { test } from "node:test";
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
FlatkeyError,
|
|
6
|
-
generateAudio,
|
|
7
|
-
generateImage,
|
|
8
|
-
generateText,
|
|
9
|
-
generateVideo,
|
|
10
|
-
getCredits,
|
|
11
|
-
getModels,
|
|
12
|
-
getStatus,
|
|
13
|
-
planImageRequest,
|
|
14
|
-
planAudioMusicRequest,
|
|
15
|
-
planTextRequest,
|
|
16
|
-
planAudioSfxRequest,
|
|
17
|
-
planVoicesRequest,
|
|
18
|
-
planVideoRequest,
|
|
19
|
-
} from "../src/api.js";
|
|
20
|
-
|
|
21
|
-
function fetchRecorder(responseBody = { ok: true }) {
|
|
22
|
-
const calls = [];
|
|
23
|
-
const fetch = async (url, init) => {
|
|
24
|
-
calls.push({ url, init });
|
|
25
|
-
return {
|
|
26
|
-
ok: true,
|
|
27
|
-
status: 200,
|
|
28
|
-
async arrayBuffer() {
|
|
29
|
-
return Buffer.from("binary");
|
|
30
|
-
},
|
|
31
|
-
async json() {
|
|
32
|
-
return responseBody;
|
|
33
|
-
},
|
|
34
|
-
};
|
|
35
|
-
};
|
|
36
|
-
return { fetch, calls };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
test("builds nano image request using gemini-style route", async () => {
|
|
40
|
-
const { fetch, calls } = fetchRecorder({ candidates: [] });
|
|
41
|
-
|
|
42
|
-
await generateImage({
|
|
43
|
-
apiKey: "key",
|
|
44
|
-
baseUrl: "https://router.test",
|
|
45
|
-
prompt: "poster",
|
|
46
|
-
fetch,
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
assert.equal(
|
|
50
|
-
calls[0].url,
|
|
51
|
-
"https://router.test/v1beta/models/nano-banana-pro-preview:generateContent?key=key",
|
|
52
|
-
);
|
|
53
|
-
assert.equal(calls[0].init.method, "POST");
|
|
54
|
-
assert.equal(calls[0].init.headers.Authorization, "Bearer key");
|
|
55
|
-
assert.equal(calls[0].init.headers["x-goog-api-key"], "key");
|
|
56
|
-
assert.deepEqual(JSON.parse(calls[0].init.body), {
|
|
57
|
-
contents: [{ parts: [{ text: "poster" }] }],
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test("builds OpenAI image request for gpt image models", async () => {
|
|
62
|
-
const { fetch, calls } = fetchRecorder({ data: [] });
|
|
63
|
-
|
|
64
|
-
await generateImage({
|
|
65
|
-
apiKey: "key",
|
|
66
|
-
baseUrl: "https://router.test",
|
|
67
|
-
model: "gpt-image-2",
|
|
68
|
-
prompt: "cover",
|
|
69
|
-
size: "1024x1024",
|
|
70
|
-
n: "2",
|
|
71
|
-
fetch,
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
assert.equal(calls[0].url, "https://router.test/v1/images/generations");
|
|
75
|
-
assert.equal(calls[0].init.headers.Authorization, "Bearer key");
|
|
76
|
-
assert.deepEqual(JSON.parse(calls[0].init.body), {
|
|
77
|
-
model: "gpt-image-2",
|
|
78
|
-
prompt: "cover",
|
|
79
|
-
size: "1024x1024",
|
|
80
|
-
n: 2,
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test("builds video generation request", async () => {
|
|
85
|
-
const { fetch, calls } = fetchRecorder({ data: [] });
|
|
86
|
-
|
|
87
|
-
await generateVideo({
|
|
88
|
-
apiKey: "key",
|
|
89
|
-
baseUrl: "https://router.test",
|
|
90
|
-
model: "veo-3",
|
|
91
|
-
prompt: "walkthrough",
|
|
92
|
-
duration: "8",
|
|
93
|
-
aspect: "16:9",
|
|
94
|
-
fps: "24",
|
|
95
|
-
fetch,
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
assert.equal(calls[0].url, "https://router.test/v1/video/generations");
|
|
99
|
-
assert.deepEqual(JSON.parse(calls[0].init.body), {
|
|
100
|
-
model: "veo-3",
|
|
101
|
-
prompt: "walkthrough",
|
|
102
|
-
duration: 8,
|
|
103
|
-
aspect: "16:9",
|
|
104
|
-
fps: 24,
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
test("builds seedance2 video generation request", async () => {
|
|
109
|
-
const { fetch, calls } = fetchRecorder({ data: [] });
|
|
110
|
-
|
|
111
|
-
await generateVideo({
|
|
112
|
-
apiKey: "key",
|
|
113
|
-
baseUrl: "https://router.test",
|
|
114
|
-
model: "seedance2",
|
|
115
|
-
prompt: "newsroom b-roll",
|
|
116
|
-
fetch,
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
assert.equal(calls[0].url, "https://router.test/v1/video/generations");
|
|
120
|
-
assert.equal(JSON.parse(calls[0].init.body).model, "seedance2");
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
test("builds text generation request for gpt-5.5", async () => {
|
|
124
|
-
const { fetch, calls } = fetchRecorder({ choices: [{ message: { content: "copy" } }] });
|
|
125
|
-
|
|
126
|
-
await generateText({
|
|
127
|
-
apiKey: "key",
|
|
128
|
-
baseUrl: "https://router.test",
|
|
129
|
-
model: "gpt-5.5",
|
|
130
|
-
prompt: "write headline",
|
|
131
|
-
fetch,
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
assert.equal(calls[0].url, "https://router.test/v1/chat/completions");
|
|
135
|
-
assert.deepEqual(JSON.parse(calls[0].init.body), {
|
|
136
|
-
model: "gpt-5.5",
|
|
137
|
-
messages: [{ role: "user", content: "write headline" }],
|
|
138
|
-
});
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
test("builds audio generation request", async () => {
|
|
142
|
-
const { fetch, calls } = fetchRecorder({ data: [] });
|
|
143
|
-
|
|
144
|
-
await generateAudio({
|
|
145
|
-
apiKey: "key",
|
|
146
|
-
baseUrl: "https://router.test",
|
|
147
|
-
model: "eleven_multilingual_v2",
|
|
148
|
-
prompt: "voiceover",
|
|
149
|
-
voiceId: "voice-123",
|
|
150
|
-
stability: "0.5",
|
|
151
|
-
similarityBoost: "0.75",
|
|
152
|
-
style: "0",
|
|
153
|
-
fetch,
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
assert.equal(calls[0].url, "https://router.test/v1/text-to-speech/voice-123");
|
|
157
|
-
assert.deepEqual(JSON.parse(calls[0].init.body), {
|
|
158
|
-
text: "voiceover",
|
|
159
|
-
model_id: "eleven_multilingual_v2",
|
|
160
|
-
voice_settings: {
|
|
161
|
-
stability: 0.5,
|
|
162
|
-
similarity_boost: 0.75,
|
|
163
|
-
style: 0,
|
|
164
|
-
},
|
|
165
|
-
});
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
test("builds audio sfx, music, and voices requests", () => {
|
|
169
|
-
assert.equal(planAudioSfxRequest({
|
|
170
|
-
apiKey: "key",
|
|
171
|
-
baseUrl: "https://router.test",
|
|
172
|
-
prompt: "glass shattering",
|
|
173
|
-
duration: "3",
|
|
174
|
-
}).url, "https://router.test/v1/sound-generation");
|
|
175
|
-
assert.deepEqual(planAudioSfxRequest({
|
|
176
|
-
apiKey: "key",
|
|
177
|
-
baseUrl: "https://router.test",
|
|
178
|
-
prompt: "glass shattering",
|
|
179
|
-
duration: "3",
|
|
180
|
-
}).body, {
|
|
181
|
-
text: "glass shattering",
|
|
182
|
-
duration_seconds: 3,
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
assert.equal(planAudioMusicRequest({
|
|
186
|
-
apiKey: "key",
|
|
187
|
-
baseUrl: "https://router.test",
|
|
188
|
-
prompt: "calm ambient piano",
|
|
189
|
-
musicLengthMs: "10000",
|
|
190
|
-
}).url, "https://router.test/v1/music");
|
|
191
|
-
assert.deepEqual(planAudioMusicRequest({
|
|
192
|
-
apiKey: "key",
|
|
193
|
-
baseUrl: "https://router.test",
|
|
194
|
-
prompt: "calm ambient piano",
|
|
195
|
-
musicLengthMs: "10000",
|
|
196
|
-
}).body, {
|
|
197
|
-
prompt: "calm ambient piano",
|
|
198
|
-
music_length_ms: 10000,
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
assert.equal(planVoicesRequest({
|
|
202
|
-
apiKey: "key",
|
|
203
|
-
baseUrl: "https://router.test",
|
|
204
|
-
}).url, "https://router.test/v1/voices");
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
test("builds credits, status, and models requests", async () => {
|
|
208
|
-
const { fetch, calls } = fetchRecorder({});
|
|
209
|
-
|
|
210
|
-
await getCredits({ apiKey: "key", baseUrl: "https://router.test", fetch });
|
|
211
|
-
await getStatus({ apiKey: "key", baseUrl: "https://router.test", fetch });
|
|
212
|
-
await getModels({ apiKey: "key", baseUrl: "https://router.test", fetch });
|
|
213
|
-
|
|
214
|
-
assert.equal(calls[0].url, "https://router.test/v1/credits");
|
|
215
|
-
assert.equal(calls[1].url, "https://router.test/v1/status");
|
|
216
|
-
assert.equal(calls[2].url, "https://router.test/v1/available_models");
|
|
217
|
-
assert.equal(calls[0].init.headers.Authorization, "Bearer key");
|
|
218
|
-
assert.equal(calls[2].init.headers.Authorization, "Bearer key");
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
test("plans available model list request from new-api relay route", async () => {
|
|
222
|
-
const { fetch, calls } = fetchRecorder({
|
|
223
|
-
success: true,
|
|
224
|
-
object: "list",
|
|
225
|
-
data: [{ id: "gpt-5.5", object: "model", owned_by: "flatkey" }],
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
await getModels({ apiKey: "env-key", fetch });
|
|
229
|
-
|
|
230
|
-
assert.equal(calls[0].url, "https://console.flatkey.ai/v1/available_models");
|
|
231
|
-
assert.equal(calls[0].init.method, "GET");
|
|
232
|
-
assert.equal(calls[0].init.headers.Authorization, "Bearer env-key");
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
test("honors explicit base url for available model list request", async () => {
|
|
236
|
-
const { fetch, calls } = fetchRecorder({ success: true, object: "list", data: [] });
|
|
237
|
-
|
|
238
|
-
await getModels({ apiKey: "env-key", baseUrl: "https://router.test", fetch });
|
|
239
|
-
|
|
240
|
-
assert.equal(calls[0].url, "https://router.test/v1/available_models");
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
test("plans dry-run requests for target media models", () => {
|
|
244
|
-
assert.equal(planImageRequest({
|
|
245
|
-
apiKey: "key",
|
|
246
|
-
model: "gpt-image-2",
|
|
247
|
-
prompt: "poster",
|
|
248
|
-
}).url, "https://router.flatkey.ai/v1/images/generations");
|
|
249
|
-
|
|
250
|
-
assert.match(planImageRequest({
|
|
251
|
-
apiKey: "key",
|
|
252
|
-
model: "nano-banana-pro-preview",
|
|
253
|
-
prompt: "poster",
|
|
254
|
-
}).url, /\/v1beta\/models\/nano-banana-pro-preview:generateContent\?key=key$/);
|
|
255
|
-
|
|
256
|
-
assert.equal(planVideoRequest({
|
|
257
|
-
apiKey: "key",
|
|
258
|
-
model: "seedance2",
|
|
259
|
-
prompt: "clip",
|
|
260
|
-
}).body.model, "seedance2");
|
|
261
|
-
|
|
262
|
-
assert.equal(planTextRequest({
|
|
263
|
-
apiKey: "key",
|
|
264
|
-
model: "gpt-5.5",
|
|
265
|
-
prompt: "headline",
|
|
266
|
-
}).body.model, "gpt-5.5");
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
test("throws FlatkeyError with API message on HTTP failure", async () => {
|
|
270
|
-
const fetch = async () => ({
|
|
271
|
-
ok: false,
|
|
272
|
-
status: 402,
|
|
273
|
-
async json() {
|
|
274
|
-
return { error: { message: "credits exhausted" } };
|
|
275
|
-
},
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
await assert.rejects(
|
|
279
|
-
() => getCredits({ apiKey: "key", baseUrl: "https://router.test", fetch }),
|
|
280
|
-
(error) => error instanceof FlatkeyError && /credits exhausted/.test(error.message),
|
|
281
|
-
);
|
|
282
|
-
});
|
package/test/artifacts.test.js
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdtemp, readFile } from "node:fs/promises";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { test } from "node:test";
|
|
6
|
-
|
|
7
|
-
import { persistArtifacts } from "../src/artifacts.js";
|
|
8
|
-
|
|
9
|
-
test("saves OpenAI base64 image artifacts", async () => {
|
|
10
|
-
const outDir = await mkdtemp(join(tmpdir(), "flatkey-artifacts-"));
|
|
11
|
-
|
|
12
|
-
const artifacts = await persistArtifacts({
|
|
13
|
-
kind: "image",
|
|
14
|
-
response: { data: [{ b64_json: Buffer.from("png-bytes").toString("base64") }] },
|
|
15
|
-
outDir,
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
assert.equal(artifacts.length, 1);
|
|
19
|
-
assert.match(artifacts[0].path, /image-01\.png$/);
|
|
20
|
-
assert.equal(await readFile(artifacts[0].path, "utf8"), "png-bytes");
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
test("saves data url video artifacts with mime extension", async () => {
|
|
24
|
-
const outDir = await mkdtemp(join(tmpdir(), "flatkey-artifacts-"));
|
|
25
|
-
|
|
26
|
-
const artifacts = await persistArtifacts({
|
|
27
|
-
kind: "video",
|
|
28
|
-
response: {
|
|
29
|
-
data: [{ url: `data:video/mp4;base64,${Buffer.from("mp4-bytes").toString("base64")}` }],
|
|
30
|
-
},
|
|
31
|
-
outDir,
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
assert.match(artifacts[0].path, /video-01\.mp4$/);
|
|
35
|
-
assert.equal(await readFile(artifacts[0].path, "utf8"), "mp4-bytes");
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test("preserves remote url artifacts without downloading", async () => {
|
|
39
|
-
const outDir = await mkdtemp(join(tmpdir(), "flatkey-artifacts-"));
|
|
40
|
-
|
|
41
|
-
const artifacts = await persistArtifacts({
|
|
42
|
-
kind: "audio",
|
|
43
|
-
response: { data: [{ url: "https://cdn.test/audio.mp3" }] },
|
|
44
|
-
outDir,
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
assert.deepEqual(artifacts, [{ url: "https://cdn.test/audio.mp3" }]);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test("saves first artifact to explicit output path", async () => {
|
|
51
|
-
const outDir = await mkdtemp(join(tmpdir(), "flatkey-artifacts-"));
|
|
52
|
-
const output = join(outDir, "custom.png");
|
|
53
|
-
|
|
54
|
-
const artifacts = await persistArtifacts({
|
|
55
|
-
kind: "image",
|
|
56
|
-
response: { data: [{ b64_json: Buffer.from("custom-bytes").toString("base64") }] },
|
|
57
|
-
output,
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
assert.deepEqual(artifacts, [{ path: output }]);
|
|
61
|
-
assert.equal(await readFile(output, "utf8"), "custom-bytes");
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test("adds numeric suffix for multiple artifacts with explicit output path", async () => {
|
|
65
|
-
const outDir = await mkdtemp(join(tmpdir(), "flatkey-artifacts-"));
|
|
66
|
-
const output = join(outDir, "custom.png");
|
|
67
|
-
|
|
68
|
-
const artifacts = await persistArtifacts({
|
|
69
|
-
kind: "image",
|
|
70
|
-
response: {
|
|
71
|
-
data: [
|
|
72
|
-
{ b64_json: Buffer.from("one").toString("base64") },
|
|
73
|
-
{ b64_json: Buffer.from("two").toString("base64") },
|
|
74
|
-
],
|
|
75
|
-
},
|
|
76
|
-
output,
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
assert.match(artifacts[0].path, /custom\.png$/);
|
|
80
|
-
assert.match(artifacts[1].path, /custom-02\.png$/);
|
|
81
|
-
assert.equal(await readFile(artifacts[1].path, "utf8"), "two");
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test("downloads remote url artifact when explicit output path is set", async () => {
|
|
85
|
-
const outDir = await mkdtemp(join(tmpdir(), "flatkey-artifacts-"));
|
|
86
|
-
const output = join(outDir, "clip.mp4");
|
|
87
|
-
const calls = [];
|
|
88
|
-
|
|
89
|
-
const artifacts = await persistArtifacts({
|
|
90
|
-
kind: "video",
|
|
91
|
-
response: { data: [{ url: "https://cdn.test/clip.mp4" }] },
|
|
92
|
-
output,
|
|
93
|
-
fetch: async (url) => {
|
|
94
|
-
calls.push(url);
|
|
95
|
-
return {
|
|
96
|
-
ok: true,
|
|
97
|
-
status: 200,
|
|
98
|
-
async arrayBuffer() {
|
|
99
|
-
return Buffer.from("video-bytes");
|
|
100
|
-
},
|
|
101
|
-
};
|
|
102
|
-
},
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
assert.deepEqual(calls, ["https://cdn.test/clip.mp4"]);
|
|
106
|
-
assert.deepEqual(artifacts, [{ path: output }]);
|
|
107
|
-
assert.equal(await readFile(output, "utf8"), "video-bytes");
|
|
108
|
-
});
|