ai-cli 0.1.0 → 0.2.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/README.md +147 -0
- package/package.json +1 -1
- package/src/cli.test.ts +1 -26
- package/src/commands/image.ts +62 -5
- package/src/commands/models.ts +36 -47
- package/src/commands/text.ts +7 -2
- package/src/commands/video.ts +7 -2
- package/src/index.ts +0 -2
- package/src/lib/models.test.ts +236 -126
- package/src/lib/models.ts +128 -118
- package/src/commands/completions.ts +0 -296
package/src/lib/models.test.ts
CHANGED
|
@@ -1,13 +1,32 @@
|
|
|
1
|
-
import { describe, expect, mock, test } from "bun:test";
|
|
1
|
+
import { afterEach, describe, expect, mock, test } from "bun:test";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
resolveModels,
|
|
5
5
|
fetchGatewayModels,
|
|
6
|
-
|
|
7
|
-
FALLBACK_IMAGE_MODELS,
|
|
8
|
-
FALLBACK_VIDEO_MODELS,
|
|
6
|
+
resetGatewayCache,
|
|
9
7
|
} from "./models.js";
|
|
10
8
|
|
|
9
|
+
const originalFetch = globalThis.fetch;
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
globalThis.fetch = originalFetch;
|
|
13
|
+
resetGatewayCache();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
function mockGateway(models: Record<string, unknown>[]) {
|
|
17
|
+
globalThis.fetch = mock(() =>
|
|
18
|
+
Promise.resolve(
|
|
19
|
+
new Response(JSON.stringify({ data: models }), { status: 200 })
|
|
20
|
+
)
|
|
21
|
+
) as unknown as typeof fetch;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function mockGatewayError() {
|
|
25
|
+
globalThis.fetch = mock(() =>
|
|
26
|
+
Promise.reject(new Error("network error"))
|
|
27
|
+
) as unknown as typeof fetch;
|
|
28
|
+
}
|
|
29
|
+
|
|
11
30
|
describe("resolveModels", () => {
|
|
12
31
|
test("returns default when no user model", () => {
|
|
13
32
|
expect(resolveModels("text")[0]).toContain("/");
|
|
@@ -22,32 +41,24 @@ describe("resolveModels", () => {
|
|
|
22
41
|
]);
|
|
23
42
|
});
|
|
24
43
|
|
|
25
|
-
test("expands short
|
|
26
|
-
|
|
44
|
+
test("expands short names when knownModels provided", () => {
|
|
45
|
+
const known = [{ id: "openai/gpt-image-1" }, { id: "bfl/flux-2-pro" }];
|
|
46
|
+
expect(resolveModels("image", "gpt-image-1", known)).toEqual([
|
|
27
47
|
"openai/gpt-image-1",
|
|
28
48
|
]);
|
|
29
|
-
expect(resolveModels("image", "flux-2-pro")).toEqual([
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
test("expands short video model names", () => {
|
|
33
|
-
expect(resolveModels("video", "seedance-2.0")).toEqual([
|
|
34
|
-
"bytedance/seedance-2.0",
|
|
49
|
+
expect(resolveModels("image", "flux-2-pro", known)).toEqual([
|
|
50
|
+
"bfl/flux-2-pro",
|
|
35
51
|
]);
|
|
36
52
|
});
|
|
37
53
|
|
|
38
|
-
test("
|
|
39
|
-
expect(resolveModels("text", "gpt-5.5")).toEqual(["openai/gpt-5.5"]);
|
|
40
|
-
expect(resolveModels("text", "o3")).toEqual(["openai/o3"]);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test("returns unknown short names as-is for text", () => {
|
|
44
|
-
expect(resolveModels("text", "gpt-image-1")).toEqual(["gpt-image-1"]);
|
|
54
|
+
test("returns unknown short names as-is when no knownModels", () => {
|
|
45
55
|
expect(resolveModels("text", "my-model")).toEqual(["my-model"]);
|
|
46
56
|
});
|
|
47
57
|
|
|
48
|
-
test("returns unknown short names as-is
|
|
49
|
-
|
|
50
|
-
|
|
58
|
+
test("returns unknown short names as-is when not in knownModels", () => {
|
|
59
|
+
const known = [{ id: "openai/gpt-5" }];
|
|
60
|
+
expect(resolveModels("text", "nonexistent", known)).toEqual([
|
|
61
|
+
"nonexistent",
|
|
51
62
|
]);
|
|
52
63
|
});
|
|
53
64
|
});
|
|
@@ -73,7 +84,8 @@ describe("resolveModels multi", () => {
|
|
|
73
84
|
});
|
|
74
85
|
|
|
75
86
|
test("expands short names in comma list", () => {
|
|
76
|
-
const
|
|
87
|
+
const known = [{ id: "openai/gpt-image-1" }, { id: "bfl/flux-2-pro" }];
|
|
88
|
+
const result = resolveModels("image", "gpt-image-1,flux-2-pro", known);
|
|
77
89
|
expect(result).toEqual(["openai/gpt-image-1", "bfl/flux-2-pro"]);
|
|
78
90
|
});
|
|
79
91
|
|
|
@@ -90,108 +102,206 @@ describe("resolveModels multi", () => {
|
|
|
90
102
|
});
|
|
91
103
|
|
|
92
104
|
describe("fetchGatewayModels", () => {
|
|
93
|
-
test("partitions models by
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
test("
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
)
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
105
|
+
test("partitions models by type with enriched fields", async () => {
|
|
106
|
+
mockGateway([
|
|
107
|
+
{
|
|
108
|
+
id: "openai/gpt-5",
|
|
109
|
+
name: "GPT 5",
|
|
110
|
+
owned_by: "openai",
|
|
111
|
+
type: "language",
|
|
112
|
+
tags: [],
|
|
113
|
+
pricing: { input: "0.000003", output: "0.000015" },
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: "openai/gpt-image-2",
|
|
117
|
+
name: "GPT Image 2",
|
|
118
|
+
description: "Image gen",
|
|
119
|
+
owned_by: "openai",
|
|
120
|
+
type: "image",
|
|
121
|
+
tags: ["image-generation"],
|
|
122
|
+
pricing: { image: "0.02" },
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
id: "google/veo-3.0",
|
|
126
|
+
name: "Veo 3",
|
|
127
|
+
owned_by: "google",
|
|
128
|
+
type: "video",
|
|
129
|
+
tags: [],
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: "openai/text-embedding-3",
|
|
133
|
+
name: "Embedding",
|
|
134
|
+
owned_by: "openai",
|
|
135
|
+
type: "embedding",
|
|
136
|
+
tags: [],
|
|
137
|
+
},
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
const result = await fetchGatewayModels();
|
|
141
|
+
|
|
142
|
+
expect(result.text).toHaveLength(1);
|
|
143
|
+
expect(result.text[0].id).toBe("openai/gpt-5");
|
|
144
|
+
expect(result.text[0].creator).toBe("openai");
|
|
145
|
+
expect(result.text[0].capabilities).toEqual(["text"]);
|
|
146
|
+
expect(result.text[0].pricing).toEqual({
|
|
147
|
+
input: "0.000003",
|
|
148
|
+
output: "0.000015",
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
expect(result.image).toHaveLength(1);
|
|
152
|
+
expect(result.image[0].id).toBe("openai/gpt-image-2");
|
|
153
|
+
expect(result.image[0].creator).toBe("openai");
|
|
154
|
+
expect(result.image[0].capabilities).toEqual(["image"]);
|
|
155
|
+
expect(result.image[0].description).toBe("Image gen");
|
|
156
|
+
expect(result.image[0].pricing).toEqual({ image: "0.02" });
|
|
157
|
+
|
|
158
|
+
expect(result.video).toHaveLength(1);
|
|
159
|
+
expect(result.video[0].id).toBe("google/veo-3.0");
|
|
160
|
+
expect(result.video[0].creator).toBe("google");
|
|
161
|
+
expect(result.video[0].capabilities).toEqual(["video"]);
|
|
162
|
+
|
|
163
|
+
// embedding type is excluded
|
|
164
|
+
expect(result.all).toHaveLength(3);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("language models with image-generation tag appear in both text and image", async () => {
|
|
168
|
+
mockGateway([
|
|
169
|
+
{
|
|
170
|
+
id: "google/gemini-2.5-flash-image",
|
|
171
|
+
name: "Gemini Flash Image",
|
|
172
|
+
owned_by: "google",
|
|
173
|
+
type: "language",
|
|
174
|
+
tags: ["image-generation"],
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
id: "openai/gpt-image-2",
|
|
178
|
+
name: "GPT Image 2",
|
|
179
|
+
owned_by: "openai",
|
|
180
|
+
type: "image",
|
|
181
|
+
tags: ["image-generation"],
|
|
182
|
+
},
|
|
183
|
+
]);
|
|
184
|
+
|
|
185
|
+
const result = await fetchGatewayModels();
|
|
186
|
+
|
|
187
|
+
expect(result.text.map((m) => m.id)).toContain(
|
|
188
|
+
"google/gemini-2.5-flash-image"
|
|
189
|
+
);
|
|
190
|
+
expect(result.image.map((m) => m.id)).toContain(
|
|
191
|
+
"google/gemini-2.5-flash-image"
|
|
192
|
+
);
|
|
193
|
+
expect(result.image.map((m) => m.id)).toContain("openai/gpt-image-2");
|
|
194
|
+
|
|
195
|
+
const gemini = result.all.find(
|
|
196
|
+
(m) => m.id === "google/gemini-2.5-flash-image"
|
|
197
|
+
)!;
|
|
198
|
+
expect(gemini.capabilities).toEqual(["text", "image"]);
|
|
199
|
+
|
|
200
|
+
expect(
|
|
201
|
+
result.languageImageModelIds.has("google/gemini-2.5-flash-image")
|
|
202
|
+
).toBe(true);
|
|
203
|
+
expect(result.languageImageModelIds.has("openai/gpt-image-2")).toBe(false);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("language models without image-generation tag stay in text only", async () => {
|
|
207
|
+
mockGateway([
|
|
208
|
+
{
|
|
209
|
+
id: "openai/gpt-5",
|
|
210
|
+
name: "GPT 5",
|
|
211
|
+
owned_by: "openai",
|
|
212
|
+
type: "language",
|
|
213
|
+
tags: ["tool-use"],
|
|
214
|
+
},
|
|
215
|
+
]);
|
|
216
|
+
|
|
217
|
+
const result = await fetchGatewayModels();
|
|
218
|
+
|
|
219
|
+
expect(result.text).toHaveLength(1);
|
|
220
|
+
expect(result.image).toHaveLength(0);
|
|
221
|
+
expect(result.languageImageModelIds.size).toBe(0);
|
|
222
|
+
expect(result.all[0].capabilities).toEqual(["text"]);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("returns empty lists on gateway error", async () => {
|
|
226
|
+
mockGatewayError();
|
|
227
|
+
|
|
228
|
+
const result = await fetchGatewayModels();
|
|
229
|
+
|
|
230
|
+
expect(result.text).toHaveLength(0);
|
|
231
|
+
expect(result.image).toHaveLength(0);
|
|
232
|
+
expect(result.video).toHaveLength(0);
|
|
233
|
+
expect(result.all).toHaveLength(0);
|
|
234
|
+
expect(result.languageImageModelIds.size).toBe(0);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("returns empty lists on non-200 response", async () => {
|
|
238
|
+
globalThis.fetch = mock(() =>
|
|
239
|
+
Promise.resolve(new Response("Not Found", { status: 404 }))
|
|
240
|
+
) as unknown as typeof fetch;
|
|
241
|
+
|
|
242
|
+
const result = await fetchGatewayModels();
|
|
243
|
+
|
|
244
|
+
expect(result.text).toHaveLength(0);
|
|
245
|
+
expect(result.image).toHaveLength(0);
|
|
246
|
+
expect(result.video).toHaveLength(0);
|
|
247
|
+
expect(result.all).toHaveLength(0);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("caches result across multiple calls", async () => {
|
|
251
|
+
const fetchMock = mock(() =>
|
|
252
|
+
Promise.resolve(
|
|
253
|
+
new Response(
|
|
254
|
+
JSON.stringify({
|
|
255
|
+
data: [
|
|
256
|
+
{
|
|
257
|
+
id: "openai/gpt-5",
|
|
258
|
+
name: "GPT 5",
|
|
259
|
+
owned_by: "openai",
|
|
260
|
+
type: "language",
|
|
261
|
+
tags: [],
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
}),
|
|
265
|
+
{ status: 200 }
|
|
266
|
+
)
|
|
267
|
+
)
|
|
268
|
+
);
|
|
269
|
+
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
|
270
|
+
|
|
271
|
+
const r1 = await fetchGatewayModels();
|
|
272
|
+
const r2 = await fetchGatewayModels();
|
|
273
|
+
|
|
274
|
+
expect(r1).toBe(r2);
|
|
275
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("pricing is omitted when all pricing fields are empty", async () => {
|
|
279
|
+
mockGateway([
|
|
280
|
+
{
|
|
281
|
+
id: "openai/gpt-5",
|
|
282
|
+
name: "GPT 5",
|
|
283
|
+
owned_by: "openai",
|
|
284
|
+
type: "language",
|
|
285
|
+
tags: [],
|
|
286
|
+
pricing: {},
|
|
287
|
+
},
|
|
288
|
+
]);
|
|
289
|
+
|
|
290
|
+
const result = await fetchGatewayModels();
|
|
291
|
+
expect(result.text[0].pricing).toBeUndefined();
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("falls back to parsing creator from id when owned_by is absent", async () => {
|
|
295
|
+
mockGateway([
|
|
296
|
+
{
|
|
297
|
+
id: "openai/gpt-5",
|
|
298
|
+
name: "GPT 5",
|
|
299
|
+
type: "language",
|
|
300
|
+
tags: [],
|
|
301
|
+
},
|
|
302
|
+
]);
|
|
303
|
+
|
|
304
|
+
const result = await fetchGatewayModels();
|
|
305
|
+
expect(result.text[0].creator).toBe("openai");
|
|
196
306
|
});
|
|
197
307
|
});
|