voyageai-cli 1.6.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.
- package/README.md +4 -3
- package/package.json +2 -3
- package/src/cli.js +4 -0
- package/src/commands/benchmark.js +799 -0
- package/src/commands/playground.js +236 -0
- package/src/lib/explanations.js +47 -0
- package/src/lib/ui.js +53 -4
- package/src/playground/index.html +1111 -0
- package/test/commands/benchmark.test.js +252 -0
- package/test/commands/playground.test.js +137 -0
- package/test/lib/explanations.test.js +1 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { describe, it } = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const { Command } = require('commander');
|
|
6
|
+
const { registerBenchmark } = require('../../src/commands/benchmark');
|
|
7
|
+
|
|
8
|
+
describe('benchmark command', () => {
|
|
9
|
+
it('registers as benchmark with bench alias', () => {
|
|
10
|
+
const program = new Command();
|
|
11
|
+
registerBenchmark(program);
|
|
12
|
+
const benchCmd = program.commands.find(c => c.name() === 'benchmark');
|
|
13
|
+
assert.ok(benchCmd, 'benchmark command should be registered');
|
|
14
|
+
assert.ok(benchCmd.aliases().includes('bench'), 'should have "bench" alias');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('has embed subcommand', () => {
|
|
18
|
+
const program = new Command();
|
|
19
|
+
registerBenchmark(program);
|
|
20
|
+
const benchCmd = program.commands.find(c => c.name() === 'benchmark');
|
|
21
|
+
const embedSub = benchCmd.commands.find(c => c.name() === 'embed');
|
|
22
|
+
assert.ok(embedSub, 'embed subcommand should be registered');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('embed has --models option', () => {
|
|
26
|
+
const program = new Command();
|
|
27
|
+
registerBenchmark(program);
|
|
28
|
+
const benchCmd = program.commands.find(c => c.name() === 'benchmark');
|
|
29
|
+
const embedSub = benchCmd.commands.find(c => c.name() === 'embed');
|
|
30
|
+
const optionNames = embedSub.options.map(o => o.long);
|
|
31
|
+
assert.ok(optionNames.includes('--models'), 'should have --models option');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('embed has --rounds option', () => {
|
|
35
|
+
const program = new Command();
|
|
36
|
+
registerBenchmark(program);
|
|
37
|
+
const benchCmd = program.commands.find(c => c.name() === 'benchmark');
|
|
38
|
+
const embedSub = benchCmd.commands.find(c => c.name() === 'embed');
|
|
39
|
+
const optionNames = embedSub.options.map(o => o.long);
|
|
40
|
+
assert.ok(optionNames.includes('--rounds'), 'should have --rounds option');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('embed has --input option for custom text', () => {
|
|
44
|
+
const program = new Command();
|
|
45
|
+
registerBenchmark(program);
|
|
46
|
+
const benchCmd = program.commands.find(c => c.name() === 'benchmark');
|
|
47
|
+
const embedSub = benchCmd.commands.find(c => c.name() === 'embed');
|
|
48
|
+
const optionNames = embedSub.options.map(o => o.long);
|
|
49
|
+
assert.ok(optionNames.includes('--input'), 'should have --input option');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('embed has --file option', () => {
|
|
53
|
+
const program = new Command();
|
|
54
|
+
registerBenchmark(program);
|
|
55
|
+
const benchCmd = program.commands.find(c => c.name() === 'benchmark');
|
|
56
|
+
const embedSub = benchCmd.commands.find(c => c.name() === 'embed');
|
|
57
|
+
const optionNames = embedSub.options.map(o => o.long);
|
|
58
|
+
assert.ok(optionNames.includes('--file'), 'should have --file option');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('embed has --json and --quiet options', () => {
|
|
62
|
+
const program = new Command();
|
|
63
|
+
registerBenchmark(program);
|
|
64
|
+
const benchCmd = program.commands.find(c => c.name() === 'benchmark');
|
|
65
|
+
const embedSub = benchCmd.commands.find(c => c.name() === 'embed');
|
|
66
|
+
const optionNames = embedSub.options.map(o => o.long);
|
|
67
|
+
assert.ok(optionNames.includes('--json'), 'should have --json option');
|
|
68
|
+
assert.ok(optionNames.includes('--quiet'), 'should have --quiet option');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('embed has --dimensions option', () => {
|
|
72
|
+
const program = new Command();
|
|
73
|
+
registerBenchmark(program);
|
|
74
|
+
const benchCmd = program.commands.find(c => c.name() === 'benchmark');
|
|
75
|
+
const embedSub = benchCmd.commands.find(c => c.name() === 'embed');
|
|
76
|
+
const optionNames = embedSub.options.map(o => o.long);
|
|
77
|
+
assert.ok(optionNames.includes('--dimensions'), 'should have --dimensions option');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('has rerank subcommand', () => {
|
|
81
|
+
const program = new Command();
|
|
82
|
+
registerBenchmark(program);
|
|
83
|
+
const benchCmd = program.commands.find(c => c.name() === 'benchmark');
|
|
84
|
+
const rerankSub = benchCmd.commands.find(c => c.name() === 'rerank');
|
|
85
|
+
assert.ok(rerankSub, 'rerank subcommand should be registered');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('rerank has --models and --rounds options', () => {
|
|
89
|
+
const program = new Command();
|
|
90
|
+
registerBenchmark(program);
|
|
91
|
+
const benchCmd = program.commands.find(c => c.name() === 'benchmark');
|
|
92
|
+
const rerankSub = benchCmd.commands.find(c => c.name() === 'rerank');
|
|
93
|
+
const optionNames = rerankSub.options.map(o => o.long);
|
|
94
|
+
assert.ok(optionNames.includes('--models'), 'should have --models option');
|
|
95
|
+
assert.ok(optionNames.includes('--rounds'), 'should have --rounds option');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('rerank has --query option', () => {
|
|
99
|
+
const program = new Command();
|
|
100
|
+
registerBenchmark(program);
|
|
101
|
+
const benchCmd = program.commands.find(c => c.name() === 'benchmark');
|
|
102
|
+
const rerankSub = benchCmd.commands.find(c => c.name() === 'rerank');
|
|
103
|
+
const optionNames = rerankSub.options.map(o => o.long);
|
|
104
|
+
assert.ok(optionNames.includes('--query'), 'should have --query option');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('rerank has --documents-file and --top-k options', () => {
|
|
108
|
+
const program = new Command();
|
|
109
|
+
registerBenchmark(program);
|
|
110
|
+
const benchCmd = program.commands.find(c => c.name() === 'benchmark');
|
|
111
|
+
const rerankSub = benchCmd.commands.find(c => c.name() === 'rerank');
|
|
112
|
+
const optionNames = rerankSub.options.map(o => o.long);
|
|
113
|
+
assert.ok(optionNames.includes('--documents-file'), 'should have --documents-file option');
|
|
114
|
+
assert.ok(optionNames.includes('--top-k'), 'should have --top-k option');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('has similarity subcommand', () => {
|
|
118
|
+
const program = new Command();
|
|
119
|
+
registerBenchmark(program);
|
|
120
|
+
const benchCmd = program.commands.find(c => c.name() === 'benchmark');
|
|
121
|
+
const simSub = benchCmd.commands.find(c => c.name() === 'similarity');
|
|
122
|
+
assert.ok(simSub, 'similarity subcommand should be registered');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('similarity has --models, --query, --file, --top-k options', () => {
|
|
126
|
+
const program = new Command();
|
|
127
|
+
registerBenchmark(program);
|
|
128
|
+
const benchCmd = program.commands.find(c => c.name() === 'benchmark');
|
|
129
|
+
const simSub = benchCmd.commands.find(c => c.name() === 'similarity');
|
|
130
|
+
const optionNames = simSub.options.map(o => o.long);
|
|
131
|
+
assert.ok(optionNames.includes('--models'), 'should have --models');
|
|
132
|
+
assert.ok(optionNames.includes('--query'), 'should have --query');
|
|
133
|
+
assert.ok(optionNames.includes('--file'), 'should have --file');
|
|
134
|
+
assert.ok(optionNames.includes('--top-k'), 'should have --top-k');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('has cost subcommand', () => {
|
|
138
|
+
const program = new Command();
|
|
139
|
+
registerBenchmark(program);
|
|
140
|
+
const benchCmd = program.commands.find(c => c.name() === 'benchmark');
|
|
141
|
+
const costSub = benchCmd.commands.find(c => c.name() === 'cost');
|
|
142
|
+
assert.ok(costSub, 'cost subcommand should be registered');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('cost has --models, --tokens, --volumes options', () => {
|
|
146
|
+
const program = new Command();
|
|
147
|
+
registerBenchmark(program);
|
|
148
|
+
const benchCmd = program.commands.find(c => c.name() === 'benchmark');
|
|
149
|
+
const costSub = benchCmd.commands.find(c => c.name() === 'cost');
|
|
150
|
+
const optionNames = costSub.options.map(o => o.long);
|
|
151
|
+
assert.ok(optionNames.includes('--models'), 'should have --models');
|
|
152
|
+
assert.ok(optionNames.includes('--tokens'), 'should have --tokens');
|
|
153
|
+
assert.ok(optionNames.includes('--volumes'), 'should have --volumes');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('has batch subcommand', () => {
|
|
157
|
+
const program = new Command();
|
|
158
|
+
registerBenchmark(program);
|
|
159
|
+
const benchCmd = program.commands.find(c => c.name() === 'benchmark');
|
|
160
|
+
const batchSub = benchCmd.commands.find(c => c.name() === 'batch');
|
|
161
|
+
assert.ok(batchSub, 'batch subcommand should be registered');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('batch has --model, --batch-sizes, --rounds options', () => {
|
|
165
|
+
const program = new Command();
|
|
166
|
+
registerBenchmark(program);
|
|
167
|
+
const benchCmd = program.commands.find(c => c.name() === 'benchmark');
|
|
168
|
+
const batchSub = benchCmd.commands.find(c => c.name() === 'batch');
|
|
169
|
+
const optionNames = batchSub.options.map(o => o.long);
|
|
170
|
+
assert.ok(optionNames.includes('--model'), 'should have --model');
|
|
171
|
+
assert.ok(optionNames.includes('--batch-sizes'), 'should have --batch-sizes');
|
|
172
|
+
assert.ok(optionNames.includes('--rounds'), 'should have --rounds');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('all subcommands have --json output option', () => {
|
|
176
|
+
const program = new Command();
|
|
177
|
+
registerBenchmark(program);
|
|
178
|
+
const benchCmd = program.commands.find(c => c.name() === 'benchmark');
|
|
179
|
+
|
|
180
|
+
for (const sub of benchCmd.commands) {
|
|
181
|
+
if (sub.name() === 'help') continue;
|
|
182
|
+
const optionNames = sub.options.map(o => o.long);
|
|
183
|
+
assert.ok(optionNames.includes('--json'), `${sub.name()} should have --json option`);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('all subcommands have --quiet option', () => {
|
|
188
|
+
const program = new Command();
|
|
189
|
+
registerBenchmark(program);
|
|
190
|
+
const benchCmd = program.commands.find(c => c.name() === 'benchmark');
|
|
191
|
+
|
|
192
|
+
for (const sub of benchCmd.commands) {
|
|
193
|
+
if (sub.name() === 'help') continue;
|
|
194
|
+
const optionNames = sub.options.map(o => o.long);
|
|
195
|
+
assert.ok(optionNames.includes('--quiet'), `${sub.name()} should have --quiet option`);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('embed defaults models to voyage-4-large,voyage-4,voyage-4-lite', () => {
|
|
200
|
+
const program = new Command();
|
|
201
|
+
registerBenchmark(program);
|
|
202
|
+
const benchCmd = program.commands.find(c => c.name() === 'benchmark');
|
|
203
|
+
const embedSub = benchCmd.commands.find(c => c.name() === 'embed');
|
|
204
|
+
const modelsOpt = embedSub.options.find(o => o.long === '--models');
|
|
205
|
+
assert.equal(modelsOpt.defaultValue, 'voyage-4-large,voyage-4,voyage-4-lite');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('rerank defaults models to rerank-2.5,rerank-2.5-lite', () => {
|
|
209
|
+
const program = new Command();
|
|
210
|
+
registerBenchmark(program);
|
|
211
|
+
const benchCmd = program.commands.find(c => c.name() === 'benchmark');
|
|
212
|
+
const rerankSub = benchCmd.commands.find(c => c.name() === 'rerank');
|
|
213
|
+
const modelsOpt = rerankSub.options.find(o => o.long === '--models');
|
|
214
|
+
assert.equal(modelsOpt.defaultValue, 'rerank-2.5,rerank-2.5-lite');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('cost defaults volumes to 100,1000,10000,100000', () => {
|
|
218
|
+
const program = new Command();
|
|
219
|
+
registerBenchmark(program);
|
|
220
|
+
const benchCmd = program.commands.find(c => c.name() === 'benchmark');
|
|
221
|
+
const costSub = benchCmd.commands.find(c => c.name() === 'cost');
|
|
222
|
+
const volOpt = costSub.options.find(o => o.long === '--volumes');
|
|
223
|
+
assert.equal(volOpt.defaultValue, '100,1000,10000,100000');
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('embed has --save option', () => {
|
|
227
|
+
const program = new Command();
|
|
228
|
+
registerBenchmark(program);
|
|
229
|
+
const benchCmd = program.commands.find(c => c.name() === 'benchmark');
|
|
230
|
+
const embedSub = benchCmd.commands.find(c => c.name() === 'embed');
|
|
231
|
+
const optionNames = embedSub.options.map(o => o.long);
|
|
232
|
+
assert.ok(optionNames.includes('--save'), 'should have --save option');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('rerank has --save option', () => {
|
|
236
|
+
const program = new Command();
|
|
237
|
+
registerBenchmark(program);
|
|
238
|
+
const benchCmd = program.commands.find(c => c.name() === 'benchmark');
|
|
239
|
+
const rerankSub = benchCmd.commands.find(c => c.name() === 'rerank');
|
|
240
|
+
const optionNames = rerankSub.options.map(o => o.long);
|
|
241
|
+
assert.ok(optionNames.includes('--save'), 'should have --save option');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('batch defaults batch-sizes to 1,5,10,25,50', () => {
|
|
245
|
+
const program = new Command();
|
|
246
|
+
registerBenchmark(program);
|
|
247
|
+
const benchCmd = program.commands.find(c => c.name() === 'benchmark');
|
|
248
|
+
const batchSub = benchCmd.commands.find(c => c.name() === 'batch');
|
|
249
|
+
const sizesOpt = batchSub.options.find(o => o.long === '--batch-sizes');
|
|
250
|
+
assert.equal(sizesOpt.defaultValue, '1,5,10,25,50');
|
|
251
|
+
});
|
|
252
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { describe, it, after } = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const http = require('http');
|
|
6
|
+
const { Command } = require('commander');
|
|
7
|
+
const { registerPlayground, createPlaygroundServer } = require('../../src/commands/playground');
|
|
8
|
+
|
|
9
|
+
describe('playground command', () => {
|
|
10
|
+
let server;
|
|
11
|
+
let port;
|
|
12
|
+
|
|
13
|
+
// Start server once for all tests
|
|
14
|
+
const serverReady = new Promise((resolve) => {
|
|
15
|
+
server = createPlaygroundServer();
|
|
16
|
+
server.listen(0, () => {
|
|
17
|
+
port = server.address().port;
|
|
18
|
+
resolve();
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
after(() => {
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
if (server) server.close(resolve);
|
|
25
|
+
else resolve();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('registers correctly on a program', () => {
|
|
30
|
+
const program = new Command();
|
|
31
|
+
registerPlayground(program);
|
|
32
|
+
const cmd = program.commands.find(c => c.name() === 'playground');
|
|
33
|
+
assert.ok(cmd, 'playground command should be registered');
|
|
34
|
+
assert.ok(cmd.description().includes('playground') || cmd.description().includes('Playground') || cmd.description().includes('web'),
|
|
35
|
+
'should have a relevant description');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('serves HTML on GET /', async () => {
|
|
39
|
+
await serverReady;
|
|
40
|
+
const body = await httpGet(`http://localhost:${port}/`);
|
|
41
|
+
assert.ok(body.includes('<!DOCTYPE html>'), 'should return HTML');
|
|
42
|
+
assert.ok(body.includes('Voyage AI Playground'), 'should include playground title');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('returns JSON from GET /api/models', async () => {
|
|
46
|
+
await serverReady;
|
|
47
|
+
const body = await httpGet(`http://localhost:${port}/api/models`);
|
|
48
|
+
const data = JSON.parse(body);
|
|
49
|
+
assert.ok(Array.isArray(data.models), 'models should be an array');
|
|
50
|
+
assert.ok(data.models.length > 0, 'should have at least one model');
|
|
51
|
+
assert.ok(data.models.every(m => !m.legacy), 'should not include legacy models');
|
|
52
|
+
// Check a known model exists
|
|
53
|
+
const names = data.models.map(m => m.name);
|
|
54
|
+
assert.ok(names.includes('voyage-4-large'), 'should include voyage-4-large');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('returns config with hasKey boolean from GET /api/config', async () => {
|
|
58
|
+
await serverReady;
|
|
59
|
+
const body = await httpGet(`http://localhost:${port}/api/config`);
|
|
60
|
+
const data = JSON.parse(body);
|
|
61
|
+
assert.ok(typeof data.hasKey === 'boolean', 'hasKey should be a boolean');
|
|
62
|
+
assert.ok(typeof data.baseUrl === 'string', 'baseUrl should be a string');
|
|
63
|
+
// Should never expose the actual key
|
|
64
|
+
assert.ok(!data.apiKey, 'should not expose apiKey');
|
|
65
|
+
assert.ok(!data.key, 'should not expose key');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('returns 404 for unknown routes', async () => {
|
|
69
|
+
await serverReady;
|
|
70
|
+
const { statusCode } = await httpGetFull(`http://localhost:${port}/nonexistent`);
|
|
71
|
+
assert.equal(statusCode, 404);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('returns 400 for POST /api/embed with invalid body', async () => {
|
|
75
|
+
await serverReady;
|
|
76
|
+
const { statusCode, body } = await httpPostFull(`http://localhost:${port}/api/embed`, { texts: 'not-an-array' });
|
|
77
|
+
assert.equal(statusCode, 400);
|
|
78
|
+
const data = JSON.parse(body);
|
|
79
|
+
assert.ok(data.error, 'should return an error message');
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Simple HTTP GET that returns the body string.
|
|
85
|
+
*/
|
|
86
|
+
function httpGet(url) {
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
http.get(url, (res) => {
|
|
89
|
+
const chunks = [];
|
|
90
|
+
res.on('data', c => chunks.push(c));
|
|
91
|
+
res.on('end', () => resolve(Buffer.concat(chunks).toString()));
|
|
92
|
+
res.on('error', reject);
|
|
93
|
+
}).on('error', reject);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* HTTP GET returning { statusCode, body }.
|
|
99
|
+
*/
|
|
100
|
+
function httpGetFull(url) {
|
|
101
|
+
return new Promise((resolve, reject) => {
|
|
102
|
+
http.get(url, (res) => {
|
|
103
|
+
const chunks = [];
|
|
104
|
+
res.on('data', c => chunks.push(c));
|
|
105
|
+
res.on('end', () => resolve({ statusCode: res.statusCode, body: Buffer.concat(chunks).toString() }));
|
|
106
|
+
res.on('error', reject);
|
|
107
|
+
}).on('error', reject);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* HTTP POST returning { statusCode, body }.
|
|
113
|
+
*/
|
|
114
|
+
function httpPostFull(url, data) {
|
|
115
|
+
return new Promise((resolve, reject) => {
|
|
116
|
+
const payload = JSON.stringify(data);
|
|
117
|
+
const parsed = new URL(url);
|
|
118
|
+
const req = http.request({
|
|
119
|
+
hostname: parsed.hostname,
|
|
120
|
+
port: parsed.port,
|
|
121
|
+
path: parsed.pathname,
|
|
122
|
+
method: 'POST',
|
|
123
|
+
headers: {
|
|
124
|
+
'Content-Type': 'application/json',
|
|
125
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
126
|
+
},
|
|
127
|
+
}, (res) => {
|
|
128
|
+
const chunks = [];
|
|
129
|
+
res.on('data', c => chunks.push(c));
|
|
130
|
+
res.on('end', () => resolve({ statusCode: res.statusCode, body: Buffer.concat(chunks).toString() }));
|
|
131
|
+
res.on('error', reject);
|
|
132
|
+
});
|
|
133
|
+
req.on('error', reject);
|
|
134
|
+
req.write(payload);
|
|
135
|
+
req.end();
|
|
136
|
+
});
|
|
137
|
+
}
|