@flatkey-ai/cli 0.1.1 → 0.1.3
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/package.json +1 -1
- package/src/api.js +10 -3
- package/src/cli.js +7 -1
- package/test/api.test.js +21 -7
- package/test/cli.test.js +9 -1
- package/test/integration.test.js +8 -4
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export const DEFAULT_BASE_URL = "https://router.flatkey.ai";
|
|
2
|
+
export const DEFAULT_MODELS_BASE_URL = "https://console.flatkey.ai";
|
|
2
3
|
|
|
3
4
|
export class FlatkeyError extends Error {
|
|
4
5
|
constructor(message, { status } = {}) {
|
|
@@ -27,7 +28,10 @@ export function planImageRequest(options) {
|
|
|
27
28
|
const path = `/v1beta/models/${encodeURIComponent(model)}:generateContent?key=${encodeURIComponent(options.apiKey)}`;
|
|
28
29
|
return planRequest(options, path, {
|
|
29
30
|
method: "POST",
|
|
30
|
-
headers: {
|
|
31
|
+
headers: {
|
|
32
|
+
...jsonHeaders(options.apiKey),
|
|
33
|
+
"x-goog-api-key": options.apiKey,
|
|
34
|
+
},
|
|
31
35
|
body: {
|
|
32
36
|
contents: [{ parts: [{ text: options.prompt }] }],
|
|
33
37
|
},
|
|
@@ -39,7 +43,7 @@ export function generateVideo(options) {
|
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
export function planVideoRequest(options) {
|
|
42
|
-
return planJsonPost(options, "/v1/
|
|
46
|
+
return planJsonPost(options, "/v1/video/generations", cleanObject({
|
|
43
47
|
model: options.model ?? "veo-3",
|
|
44
48
|
prompt: options.prompt,
|
|
45
49
|
duration: parseOptionalInteger(options.duration),
|
|
@@ -81,7 +85,10 @@ export function getStatus(options) {
|
|
|
81
85
|
}
|
|
82
86
|
|
|
83
87
|
export function getModels(options) {
|
|
84
|
-
return requestJson(
|
|
88
|
+
return requestJson({
|
|
89
|
+
...options,
|
|
90
|
+
baseUrl: options.baseUrl ?? DEFAULT_MODELS_BASE_URL,
|
|
91
|
+
}, "/v1/available_models");
|
|
85
92
|
}
|
|
86
93
|
|
|
87
94
|
async function postJson(options, path, payload) {
|
package/src/cli.js
CHANGED
|
@@ -87,7 +87,7 @@ export async function runCommand(command, deps = {}) {
|
|
|
87
87
|
return command.options.ai ? getAiHelp() : getHumanHelp();
|
|
88
88
|
}
|
|
89
89
|
if (command.group === "version") {
|
|
90
|
-
return { version:
|
|
90
|
+
return { version: await readPackageVersion() };
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
if (command.group === "models") {
|
|
@@ -244,3 +244,9 @@ function formatHuman(result) {
|
|
|
244
244
|
if (typeof result === "string") return result;
|
|
245
245
|
return JSON.stringify(result, null, 2);
|
|
246
246
|
}
|
|
247
|
+
|
|
248
|
+
async function readPackageVersion() {
|
|
249
|
+
const { readFile } = await import("node:fs/promises");
|
|
250
|
+
const packageUrl = new URL("../package.json", import.meta.url);
|
|
251
|
+
return JSON.parse(await readFile(packageUrl, "utf8")).version;
|
|
252
|
+
}
|
package/test/api.test.js
CHANGED
|
@@ -45,6 +45,8 @@ test("builds nano image request using gemini-style route", async () => {
|
|
|
45
45
|
"https://router.test/v1beta/models/nano-banana-pro-preview:generateContent?key=key",
|
|
46
46
|
);
|
|
47
47
|
assert.equal(calls[0].init.method, "POST");
|
|
48
|
+
assert.equal(calls[0].init.headers.Authorization, "Bearer key");
|
|
49
|
+
assert.equal(calls[0].init.headers["x-goog-api-key"], "key");
|
|
48
50
|
assert.deepEqual(JSON.parse(calls[0].init.body), {
|
|
49
51
|
contents: [{ parts: [{ text: "poster" }] }],
|
|
50
52
|
});
|
|
@@ -87,7 +89,7 @@ test("builds video generation request", async () => {
|
|
|
87
89
|
fetch,
|
|
88
90
|
});
|
|
89
91
|
|
|
90
|
-
assert.equal(calls[0].url, "https://router.test/v1/
|
|
92
|
+
assert.equal(calls[0].url, "https://router.test/v1/video/generations");
|
|
91
93
|
assert.deepEqual(JSON.parse(calls[0].init.body), {
|
|
92
94
|
model: "veo-3",
|
|
93
95
|
prompt: "walkthrough",
|
|
@@ -108,7 +110,7 @@ test("builds seedance2 video generation request", async () => {
|
|
|
108
110
|
fetch,
|
|
109
111
|
});
|
|
110
112
|
|
|
111
|
-
assert.equal(calls[0].url, "https://router.test/v1/
|
|
113
|
+
assert.equal(calls[0].url, "https://router.test/v1/video/generations");
|
|
112
114
|
assert.equal(JSON.parse(calls[0].init.body).model, "seedance2");
|
|
113
115
|
});
|
|
114
116
|
|
|
@@ -161,21 +163,33 @@ test("builds credits, status, and models requests", async () => {
|
|
|
161
163
|
|
|
162
164
|
assert.equal(calls[0].url, "https://router.test/v1/credits");
|
|
163
165
|
assert.equal(calls[1].url, "https://router.test/v1/status");
|
|
164
|
-
assert.equal(calls[2].url, "https://router.test/v1/
|
|
166
|
+
assert.equal(calls[2].url, "https://router.test/v1/available_models");
|
|
165
167
|
assert.equal(calls[0].init.headers.Authorization, "Bearer key");
|
|
166
168
|
assert.equal(calls[2].init.headers.Authorization, "Bearer key");
|
|
167
169
|
});
|
|
168
170
|
|
|
169
|
-
test("plans model list request from new-api relay route", async () => {
|
|
170
|
-
const { fetch, calls } = fetchRecorder({
|
|
171
|
+
test("plans available model list request from new-api relay route", async () => {
|
|
172
|
+
const { fetch, calls } = fetchRecorder({
|
|
173
|
+
success: true,
|
|
174
|
+
object: "list",
|
|
175
|
+
data: [{ id: "gpt-5.5", object: "model", owned_by: "flatkey" }],
|
|
176
|
+
});
|
|
171
177
|
|
|
172
|
-
await getModels({ apiKey: "env-key",
|
|
178
|
+
await getModels({ apiKey: "env-key", fetch });
|
|
173
179
|
|
|
174
|
-
assert.equal(calls[0].url, "https://
|
|
180
|
+
assert.equal(calls[0].url, "https://console.flatkey.ai/v1/available_models");
|
|
175
181
|
assert.equal(calls[0].init.method, "GET");
|
|
176
182
|
assert.equal(calls[0].init.headers.Authorization, "Bearer env-key");
|
|
177
183
|
});
|
|
178
184
|
|
|
185
|
+
test("honors explicit base url for available model list request", async () => {
|
|
186
|
+
const { fetch, calls } = fetchRecorder({ success: true, object: "list", data: [] });
|
|
187
|
+
|
|
188
|
+
await getModels({ apiKey: "env-key", baseUrl: "https://router.test", fetch });
|
|
189
|
+
|
|
190
|
+
assert.equal(calls[0].url, "https://router.test/v1/available_models");
|
|
191
|
+
});
|
|
192
|
+
|
|
179
193
|
test("plans dry-run requests for target media models", () => {
|
|
180
194
|
assert.equal(planImageRequest({
|
|
181
195
|
apiKey: "key",
|
package/test/cli.test.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { test } from "node:test";
|
|
3
3
|
|
|
4
|
-
import { parseArgv } from "../src/cli.js";
|
|
4
|
+
import { parseArgv, runCommand } from "../src/cli.js";
|
|
5
5
|
|
|
6
6
|
test("parses image generation with prompt and json mode", () => {
|
|
7
7
|
const command = parseArgv(["image", "generate", "--prompt", "city at dawn", "--json"]);
|
|
@@ -81,3 +81,11 @@ test("rejects unknown commands", () => {
|
|
|
81
81
|
/Unknown command: wat/,
|
|
82
82
|
);
|
|
83
83
|
});
|
|
84
|
+
|
|
85
|
+
test("version command matches package version", async () => {
|
|
86
|
+
const pkg = JSON.parse(await (await import("node:fs/promises")).readFile("package.json", "utf8"));
|
|
87
|
+
|
|
88
|
+
assert.deepEqual(await runCommand({ group: "version", options: {} }), {
|
|
89
|
+
version: pkg.version,
|
|
90
|
+
});
|
|
91
|
+
});
|
package/test/integration.test.js
CHANGED
|
@@ -20,7 +20,7 @@ test("runs generation and utility commands in json mode", async (t) => {
|
|
|
20
20
|
response.setHeader("content-type", "application/json");
|
|
21
21
|
if (request.url.startsWith("/v1beta/models/")) {
|
|
22
22
|
response.end(JSON.stringify({ data: [{ url: "https://cdn.test/image.png" }] }));
|
|
23
|
-
} else if (request.url === "/v1/
|
|
23
|
+
} else if (request.url === "/v1/video/generations") {
|
|
24
24
|
response.end(JSON.stringify({ data: [{ url: "https://cdn.test/video.mp4" }] }));
|
|
25
25
|
} else if (request.url === "/v1/audio/generations") {
|
|
26
26
|
response.end(JSON.stringify({ data: [{ url: "https://cdn.test/audio.mp3" }] }));
|
|
@@ -30,8 +30,12 @@ test("runs generation and utility commands in json mode", async (t) => {
|
|
|
30
30
|
response.end(JSON.stringify({ remaining: 42, used: 8 }));
|
|
31
31
|
} else if (request.url === "/v1/status") {
|
|
32
32
|
response.end(JSON.stringify({ status: "ok" }));
|
|
33
|
-
} else if (request.url === "/v1/
|
|
34
|
-
response.end(JSON.stringify({
|
|
33
|
+
} else if (request.url === "/v1/available_models") {
|
|
34
|
+
response.end(JSON.stringify({
|
|
35
|
+
success: true,
|
|
36
|
+
object: "list",
|
|
37
|
+
data: [{ id: "remote-image", object: "model", owned_by: "flatkey" }],
|
|
38
|
+
}));
|
|
35
39
|
} else {
|
|
36
40
|
response.statusCode = 404;
|
|
37
41
|
response.end(JSON.stringify({ error: { message: "not found" } }));
|
|
@@ -62,7 +66,7 @@ test("runs generation and utility commands in json mode", async (t) => {
|
|
|
62
66
|
]);
|
|
63
67
|
assert.equal(image.stderr, "");
|
|
64
68
|
assert.ok(requests.some((request) => request.url.startsWith("/v1beta/models/")));
|
|
65
|
-
assert.ok(requests.some((request) => request.url === "/v1/
|
|
69
|
+
assert.ok(requests.some((request) => request.url === "/v1/video/generations"));
|
|
66
70
|
assert.ok(requests.some((request) => request.url === "/v1/audio/generations"));
|
|
67
71
|
assert.ok(requests.some((request) => request.url === "/v1/chat/completions"));
|
|
68
72
|
});
|