voyageai-cli 1.5.0 → 1.6.1

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/demo.gif CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "voyageai-cli",
3
- "version": "1.5.0",
3
+ "version": "1.6.1",
4
4
  "description": "CLI for Voyage AI embeddings, reranking, and MongoDB Atlas Vector Search",
5
5
  "bin": {
6
6
  "vai": "./src/cli.js"
@@ -39,6 +39,7 @@
39
39
  "mongodb": "^6.0.0",
40
40
  "ora": "^9.1.0",
41
41
  "picocolors": "^1.1.1",
42
- "update-notifier": "^7.3.1"
42
+ "update-notifier": "^7.3.1",
43
+ "voyageai-cli": "^1.6.0"
43
44
  }
44
45
  }
@@ -17,6 +17,8 @@ function registerEmbed(program) {
17
17
  .option('-t, --input-type <type>', 'Input type: query or document')
18
18
  .option('-d, --dimensions <n>', 'Output dimensions', (v) => parseInt(v, 10))
19
19
  .option('-f, --file <path>', 'Read text from file')
20
+ .option('--truncation', 'Enable truncation for long inputs')
21
+ .option('--no-truncation', 'Disable truncation')
20
22
  .option('-o, --output-format <format>', 'Output format: json or array', 'json')
21
23
  .option('--json', 'Machine-readable JSON output')
22
24
  .option('-q, --quiet', 'Suppress non-essential output')
@@ -26,17 +28,29 @@ function registerEmbed(program) {
26
28
 
27
29
  const useColor = !opts.json;
28
30
  const useSpinner = useColor && !opts.quiet;
31
+
32
+ // Show hint when --input-type is not provided and output is interactive
33
+ if (!opts.inputType && !opts.json && !opts.quiet && process.stdout.isTTY) {
34
+ console.error(ui.dim('ℹ Tip: Use --input-type query or --input-type document for better retrieval accuracy.'));
35
+ }
36
+
29
37
  let spin;
30
38
  if (useSpinner) {
31
39
  spin = ui.spinner('Generating embeddings...');
32
40
  spin.start();
33
41
  }
34
42
 
35
- const result = await generateEmbeddings(texts, {
43
+ const embedOpts = {
36
44
  model: opts.model,
37
45
  inputType: opts.inputType,
38
46
  dimensions: opts.dimensions,
39
- });
47
+ };
48
+ // Only pass truncation when explicitly set via --truncation or --no-truncation
49
+ if (opts.truncation !== undefined) {
50
+ embedOpts.truncation = opts.truncation;
51
+ }
52
+
53
+ const result = await generateEmbeddings(texts, embedOpts);
40
54
 
41
55
  if (spin) spin.stop();
42
56
 
@@ -40,16 +40,26 @@ function registerModels(program) {
40
40
  .command('models')
41
41
  .description('List available Voyage AI models')
42
42
  .option('-t, --type <type>', 'Filter by type: embedding, reranking, or all', 'all')
43
+ .option('-a, --all', 'Show all models including legacy')
43
44
  .option('-w, --wide', 'Wide output (show all columns untruncated)')
44
45
  .option('--json', 'Machine-readable JSON output')
45
46
  .option('-q, --quiet', 'Suppress non-essential output')
46
47
  .action((opts) => {
47
48
  let models = MODEL_CATALOG;
48
49
 
50
+ // Separate current and legacy models
51
+ const showLegacy = opts.all;
52
+ const currentModels = models.filter(m => !m.legacy);
53
+ const legacyModels = models.filter(m => m.legacy);
54
+
49
55
  if (opts.type !== 'all') {
50
56
  models = models.filter(m => m.type === opts.type);
51
57
  }
52
58
 
59
+ if (!showLegacy) {
60
+ models = models.filter(m => !m.legacy);
61
+ }
62
+
53
63
  if (opts.json) {
54
64
  console.log(JSON.stringify(models, null, 2));
55
65
  return;
@@ -68,29 +78,49 @@ function registerModels(program) {
68
78
  console.log('');
69
79
  }
70
80
 
81
+ // Split models for display
82
+ const displayCurrent = models.filter(m => !m.legacy);
83
+ const displayLegacy = models.filter(m => m.legacy);
84
+
85
+ const formatWideRow = (m) => {
86
+ const name = ui.cyan(m.name);
87
+ const type = m.type === 'embedding' ? ui.green(m.type) : ui.yellow(m.type);
88
+ const price = ui.dim(m.price);
89
+ return [name, type, m.context, m.dimensions, price, m.bestFor];
90
+ };
91
+
92
+ const formatCompactRow = (m) => {
93
+ const name = ui.cyan(m.name);
94
+ const type = m.type === 'embedding' ? ui.green('embed') : ui.yellow('rerank');
95
+ const dims = compactDimensions(m.dimensions);
96
+ const price = ui.dim(compactPrice(m.price));
97
+ return [name, type, dims, price, m.shortFor || m.bestFor];
98
+ };
99
+
71
100
  if (opts.wide) {
72
- // Full table with all details
73
101
  const headers = ['Model', 'Type', 'Context', 'Dimensions', 'Price', 'Best For'];
74
- const rows = models.map(m => {
75
- const name = ui.cyan(m.name);
76
- const type = m.type === 'embedding' ? ui.green(m.type) : ui.yellow(m.type);
77
- const price = ui.dim(m.price);
78
- return [name, type, m.context, m.dimensions, price, m.bestFor];
79
- });
80
102
  const boldHeaders = headers.map(h => ui.bold(h));
103
+ const rows = displayCurrent.map(formatWideRow);
81
104
  console.log(formatTable(boldHeaders, rows));
105
+
106
+ if (showLegacy && displayLegacy.length > 0) {
107
+ console.log('');
108
+ console.log(ui.dim('Legacy Models (use latest for better quality)'));
109
+ const legacyRows = displayLegacy.map(formatWideRow);
110
+ console.log(formatTable(boldHeaders, legacyRows));
111
+ }
82
112
  } else {
83
- // Compact table — fits in 80 cols
84
113
  const headers = ['Model', 'Type', 'Dims', 'Price', 'Use Case'];
85
- const rows = models.map(m => {
86
- const name = ui.cyan(m.name);
87
- const type = m.type === 'embedding' ? ui.green('embed') : ui.yellow('rerank');
88
- const dims = compactDimensions(m.dimensions);
89
- const price = ui.dim(compactPrice(m.price));
90
- return [name, type, dims, price, m.shortFor || m.bestFor];
91
- });
92
114
  const boldHeaders = headers.map(h => ui.bold(h));
115
+ const rows = displayCurrent.map(formatCompactRow);
93
116
  console.log(formatTable(boldHeaders, rows));
117
+
118
+ if (showLegacy && displayLegacy.length > 0) {
119
+ console.log('');
120
+ console.log(ui.dim('Legacy Models (use latest for better quality)'));
121
+ const legacyRows = displayLegacy.map(formatCompactRow);
122
+ console.log(formatTable(boldHeaders, legacyRows));
123
+ }
94
124
  }
95
125
 
96
126
  if (!opts.quiet) {
@@ -18,6 +18,9 @@ function registerRerank(program) {
18
18
  .option('--documents-file <path>', 'File with documents (JSON array or newline-delimited)')
19
19
  .option('-m, --model <model>', 'Reranking model', DEFAULT_RERANK_MODEL)
20
20
  .option('-k, --top-k <n>', 'Return top K results', (v) => parseInt(v, 10))
21
+ .option('--truncation', 'Enable truncation for long inputs')
22
+ .option('--no-truncation', 'Disable truncation')
23
+ .option('--return-documents', 'Return document text in results')
21
24
  .option('--json', 'Machine-readable JSON output')
22
25
  .option('-q, --quiet', 'Suppress non-essential output')
23
26
  .action(async (opts) => {
@@ -76,6 +79,12 @@ function registerRerank(program) {
76
79
  if (opts.topK) {
77
80
  body.top_k = opts.topK;
78
81
  }
82
+ if (opts.truncation !== undefined) {
83
+ body.truncation = opts.truncation;
84
+ }
85
+ if (opts.returnDocuments) {
86
+ body.return_documents = true;
87
+ }
79
88
 
80
89
  const useColor = !opts.json;
81
90
  const useSpinner = useColor && !opts.quiet;
@@ -106,8 +115,9 @@ function registerRerank(program) {
106
115
 
107
116
  if (result.data) {
108
117
  for (const item of result.data) {
109
- const docPreview = documents[item.index].substring(0, 80);
110
- const ellipsis = documents[item.index].length > 80 ? '...' : '';
118
+ const docText = item.document || documents[item.index];
119
+ const docPreview = docText.substring(0, 80);
120
+ const ellipsis = docText.length > 80 ? '...' : '';
111
121
  console.log(`${ui.dim('[' + item.index + ']')} Score: ${ui.score(item.relevance_score)} ${ui.dim('"' + docPreview + ellipsis + '"')}`);
112
122
  }
113
123
  }
package/src/lib/api.js CHANGED
@@ -126,6 +126,7 @@ async function apiRequest(endpoint, body) {
126
126
  * @param {string} [options.model] - Model name
127
127
  * @param {string} [options.inputType] - Input type (query|document)
128
128
  * @param {number} [options.dimensions] - Output dimensions
129
+ * @param {boolean} [options.truncation] - Enable/disable truncation
129
130
  * @returns {Promise<object>} API response with embeddings
130
131
  */
131
132
  async function generateEmbeddings(texts, options = {}) {
@@ -142,6 +143,9 @@ async function generateEmbeddings(texts, options = {}) {
142
143
  if (options.dimensions) {
143
144
  body.output_dimension = options.dimensions;
144
145
  }
146
+ if (options.truncation !== undefined) {
147
+ body.truncation = options.truncation;
148
+ }
145
149
 
146
150
  return apiRequest('/embeddings', body);
147
151
  }
@@ -34,6 +34,15 @@ const MODEL_CATALOG = [
34
34
  { name: 'voyage-multimodal-3.5', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.12/M + $0.60/B px', bestFor: 'Text + images + video', shortFor: 'Multimodal' },
35
35
  { name: 'rerank-2.5', type: 'reranking', context: '32K', dimensions: '—', price: '$0.05/1M tokens', bestFor: 'Best quality reranking', shortFor: 'Best reranker' },
36
36
  { name: 'rerank-2.5-lite', type: 'reranking', context: '32K', dimensions: '—', price: '$0.02/1M tokens', bestFor: 'Fast reranking', shortFor: 'Fast reranker' },
37
+ { name: 'voyage-4-nano', type: 'embedding', context: '32K', dimensions: '512 (default), 128, 256', price: 'Open-weight', bestFor: 'Open-weight / edge', shortFor: 'Open / edge' },
38
+ // Legacy models
39
+ { name: 'voyage-3-large', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.18/1M tokens', bestFor: 'Previous gen quality', shortFor: 'Previous gen quality', legacy: true },
40
+ { name: 'voyage-3.5', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.06/1M tokens', bestFor: 'Previous gen balanced', shortFor: 'Previous gen balanced', legacy: true },
41
+ { name: 'voyage-3.5-lite', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.02/1M tokens', bestFor: 'Previous gen budget', shortFor: 'Previous gen budget', legacy: true },
42
+ { name: 'voyage-code-2', type: 'embedding', context: '16K', dimensions: '1536', price: '$0.12/1M tokens', bestFor: 'Legacy code', shortFor: 'Legacy code', legacy: true },
43
+ { name: 'voyage-multimodal-3', type: 'embedding', context: '32K', dimensions: '1024', price: '$0.12/1M tokens', bestFor: 'Legacy multimodal', shortFor: 'Legacy multimodal', legacy: true },
44
+ { name: 'rerank-2', type: 'reranking', context: '16K', dimensions: '—', price: '$0.05/1M tokens', bestFor: 'Legacy reranker', shortFor: 'Legacy reranker', legacy: true },
45
+ { name: 'rerank-2-lite', type: 'reranking', context: '8K', dimensions: '—', price: '$0.02/1M tokens', bestFor: 'Legacy fast reranker', shortFor: 'Legacy fast reranker', legacy: true },
37
46
  ];
38
47
 
39
48
  module.exports = {
@@ -210,12 +210,15 @@ const concepts = {
210
210
  `The ${pc.cyan('input_type')} parameter tells the embedding model whether the text is a`,
211
211
  `${pc.cyan('search query')} or a ${pc.cyan('document')} being indexed. This matters for retrieval quality.`,
212
212
  ``,
213
- `${pc.bold('How it works:')} Voyage AI models internally prepend a short prompt to your text`,
214
- `based on input_type:`,
215
- ` ${pc.dim('• query →')} "Represent the query for retrieving relevant documents: "`,
216
- ` ${pc.dim('• document →')} "Represent the document for retrieval: "`,
213
+ `${pc.bold(' Do not omit this parameter for retrieval tasks.')} The official docs emphasize`,
214
+ `that omitting input_type degrades retrieval accuracy.`,
217
215
  ``,
218
- `These prompts bias the embedding to be ${pc.cyan('asymmetric')} query embeddings are`,
216
+ `${pc.bold('How it works:')} Voyage AI models internally prepend a specific prompt prefix`,
217
+ `to your text based on input_type:`,
218
+ ` ${pc.dim('• query →')} ${pc.cyan('"Represent the query for retrieving supporting documents: "')}`,
219
+ ` ${pc.dim('• document →')} ${pc.cyan('"Represent the document for retrieval: "')}`,
220
+ ``,
221
+ `These prefixes bias the embedding to be ${pc.cyan('asymmetric')} — query embeddings are`,
219
222
  `optimized to find relevant documents, and document embeddings are optimized`,
220
223
  `to be found by relevant queries.`,
221
224
  ``,
@@ -226,10 +229,10 @@ const concepts = {
226
229
  `${pc.bold('When to use each:')}`,
227
230
  ` ${pc.cyan('query')} — When embedding a search query or question`,
228
231
  ` ${pc.cyan('document')} — When embedding text to be stored and searched later`,
229
- ` ${pc.dim('(omit)')} — For clustering, classification, or symmetric similarity`,
232
+ ` ${pc.dim('(omit)')} — Only for clustering, classification, or symmetric similarity`,
230
233
  ``,
231
- `${pc.bold('Tip:')} Always use ${pc.cyan('--input-type document')} when running ${pc.cyan('vai store')}, and`,
232
- `${pc.cyan('--input-type query')} is the default for ${pc.cyan('vai search')}.`,
234
+ `${pc.bold('Tip:')} Always use ${pc.cyan('--input-type document')} when running ${pc.cyan('vai store')} or`,
235
+ `${pc.cyan('vai ingest')}, and ${pc.cyan('--input-type query')} when running ${pc.cyan('vai search')}.`,
233
236
  ].join('\n'),
234
237
  links: ['https://www.mongodb.com/docs/voyageai/models/text-embeddings/'],
235
238
  tryIt: [
@@ -0,0 +1,32 @@
1
+ 'use strict';
2
+
3
+ const { describe, it, beforeEach, afterEach, mock } = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const { Command } = require('commander');
6
+ const { registerEmbed } = require('../../src/commands/embed');
7
+
8
+ describe('embed command', () => {
9
+ it('registers correctly on a program', () => {
10
+ const program = new Command();
11
+ registerEmbed(program);
12
+ const embedCmd = program.commands.find(c => c.name() === 'embed');
13
+ assert.ok(embedCmd, 'embed command should be registered');
14
+ });
15
+
16
+ it('has --truncation flag', () => {
17
+ const program = new Command();
18
+ registerEmbed(program);
19
+ const embedCmd = program.commands.find(c => c.name() === 'embed');
20
+ const optionNames = embedCmd.options.map(o => o.long);
21
+ assert.ok(optionNames.includes('--truncation'), 'should have --truncation option');
22
+ assert.ok(optionNames.includes('--no-truncation'), 'should have --no-truncation option');
23
+ });
24
+
25
+ it('has --input-type flag', () => {
26
+ const program = new Command();
27
+ registerEmbed(program);
28
+ const embedCmd = program.commands.find(c => c.name() === 'embed');
29
+ const optionNames = embedCmd.options.map(o => o.long);
30
+ assert.ok(optionNames.includes('--input-type'), 'should have --input-type option');
31
+ });
32
+ });
@@ -243,6 +243,19 @@ describe('ingest', () => {
243
243
  assert.ok(optionNames.includes('--json'), 'should have --json option');
244
244
  assert.ok(optionNames.includes('--quiet'), 'should have --quiet option');
245
245
  assert.ok(optionNames.includes('--strict'), 'should have --strict option');
246
+ assert.ok(optionNames.includes('--input-type'), 'should have --input-type option');
247
+ });
248
+
249
+ it('--input-type defaults to document', () => {
250
+ delete require.cache[require.resolve('../../src/commands/ingest')];
251
+ const { registerIngest } = require('../../src/commands/ingest');
252
+ const { Command } = require('commander');
253
+ const program = new Command();
254
+ registerIngest(program);
255
+
256
+ const ingestCmd = program.commands.find(c => c.name() === 'ingest');
257
+ const inputTypeOpt = ingestCmd.options.find(o => o.long === '--input-type');
258
+ assert.equal(inputTypeOpt.defaultValue, 'document', '--input-type should default to document');
246
259
  });
247
260
  });
248
261
  });
@@ -86,4 +86,47 @@ describe('models command', () => {
86
86
  assert.ok(parsed[0].name);
87
87
  assert.ok(parsed[0].type);
88
88
  });
89
+
90
+ it('hides legacy models by default', async () => {
91
+ const program = new Command();
92
+ program.exitOverride();
93
+ registerModels(program);
94
+
95
+ await program.parseAsync(['node', 'test', 'models', '--quiet']);
96
+
97
+ const combined = output.join('\n');
98
+ assert.ok(combined.includes('voyage-4-large'), 'Should include current models');
99
+ assert.ok(!combined.includes('voyage-3-large'), 'Should hide legacy voyage-3-large');
100
+ assert.ok(!combined.includes('rerank-2-lite'), 'Should hide legacy rerank-2-lite');
101
+ assert.ok(!combined.includes('voyage-code-2'), 'Should hide legacy voyage-code-2');
102
+ });
103
+
104
+ it('shows legacy models when --all is used', async () => {
105
+ const program = new Command();
106
+ program.exitOverride();
107
+ registerModels(program);
108
+
109
+ await program.parseAsync(['node', 'test', 'models', '--all', '--quiet']);
110
+
111
+ const combined = output.join('\n');
112
+ assert.ok(combined.includes('voyage-4-large'), 'Should include current models');
113
+ assert.ok(combined.includes('voyage-3-large'), 'Should include legacy voyage-3-large');
114
+ assert.ok(combined.includes('rerank-2'), 'Should include legacy rerank-2');
115
+ assert.ok(combined.includes('Legacy Models'), 'Should show legacy header');
116
+ });
117
+
118
+ it('--all with --json shows legacy models in JSON', async () => {
119
+ const program = new Command();
120
+ program.exitOverride();
121
+ registerModels(program);
122
+
123
+ await program.parseAsync(['node', 'test', 'models', '--all', '--json']);
124
+
125
+ const combined = output.join('\n');
126
+ const parsed = JSON.parse(combined);
127
+ assert.ok(Array.isArray(parsed));
128
+ const legacyNames = parsed.filter(m => m.legacy).map(m => m.name);
129
+ assert.ok(legacyNames.includes('voyage-3-large'), 'JSON should include legacy models');
130
+ assert.ok(legacyNames.includes('rerank-2'), 'JSON should include legacy rerankers');
131
+ });
89
132
  });
@@ -0,0 +1,32 @@
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 { registerRerank } = require('../../src/commands/rerank');
7
+
8
+ describe('rerank command', () => {
9
+ it('registers correctly on a program', () => {
10
+ const program = new Command();
11
+ registerRerank(program);
12
+ const rerankCmd = program.commands.find(c => c.name() === 'rerank');
13
+ assert.ok(rerankCmd, 'rerank command should be registered');
14
+ });
15
+
16
+ it('has --truncation flag', () => {
17
+ const program = new Command();
18
+ registerRerank(program);
19
+ const rerankCmd = program.commands.find(c => c.name() === 'rerank');
20
+ const optionNames = rerankCmd.options.map(o => o.long);
21
+ assert.ok(optionNames.includes('--truncation'), 'should have --truncation option');
22
+ assert.ok(optionNames.includes('--no-truncation'), 'should have --no-truncation option');
23
+ });
24
+
25
+ it('has --return-documents flag', () => {
26
+ const program = new Command();
27
+ registerRerank(program);
28
+ const rerankCmd = program.commands.find(c => c.name() === 'rerank');
29
+ const optionNames = rerankCmd.options.map(o => o.long);
30
+ assert.ok(optionNames.includes('--return-documents'), 'should have --return-documents option');
31
+ });
32
+ });
@@ -0,0 +1,26 @@
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 { registerStore } = require('../../src/commands/store');
7
+
8
+ describe('store command', () => {
9
+ it('registers correctly on a program', () => {
10
+ const program = new Command();
11
+ registerStore(program);
12
+ const storeCmd = program.commands.find(c => c.name() === 'store');
13
+ assert.ok(storeCmd, 'store command should be registered');
14
+ });
15
+
16
+ it('has --input-type flag defaulting to document', () => {
17
+ const program = new Command();
18
+ registerStore(program);
19
+ const storeCmd = program.commands.find(c => c.name() === 'store');
20
+ const optionNames = storeCmd.options.map(o => o.long);
21
+ assert.ok(optionNames.includes('--input-type'), 'should have --input-type option');
22
+ // Check the default value
23
+ const inputTypeOpt = storeCmd.options.find(o => o.long === '--input-type');
24
+ assert.equal(inputTypeOpt.defaultValue, 'document', '--input-type should default to document');
25
+ });
26
+ });
@@ -64,4 +64,36 @@ describe('catalog', () => {
64
64
  const rerankCount = MODEL_CATALOG.filter(m => m.type === 'reranking').length;
65
65
  assert.ok(embedCount > rerankCount);
66
66
  });
67
+
68
+ it('contains voyage-4-nano as current model', () => {
69
+ const nano = MODEL_CATALOG.find(m => m.name === 'voyage-4-nano');
70
+ assert.ok(nano, 'Should have voyage-4-nano');
71
+ assert.equal(nano.type, 'embedding');
72
+ assert.ok(!nano.legacy, 'voyage-4-nano should not be legacy');
73
+ });
74
+
75
+ it('contains legacy models with legacy flag', () => {
76
+ const legacyModels = MODEL_CATALOG.filter(m => m.legacy);
77
+ assert.ok(legacyModels.length > 0, 'Should have legacy models');
78
+
79
+ const legacyNames = legacyModels.map(m => m.name);
80
+ assert.ok(legacyNames.includes('voyage-3-large'), 'Should have voyage-3-large');
81
+ assert.ok(legacyNames.includes('voyage-3.5'), 'Should have voyage-3.5');
82
+ assert.ok(legacyNames.includes('voyage-3.5-lite'), 'Should have voyage-3.5-lite');
83
+ assert.ok(legacyNames.includes('voyage-code-2'), 'Should have voyage-code-2');
84
+ assert.ok(legacyNames.includes('voyage-multimodal-3'), 'Should have voyage-multimodal-3');
85
+ assert.ok(legacyNames.includes('rerank-2'), 'Should have rerank-2');
86
+ assert.ok(legacyNames.includes('rerank-2-lite'), 'Should have rerank-2-lite');
87
+ });
88
+
89
+ it('legacy models have required fields', () => {
90
+ const legacyModels = MODEL_CATALOG.filter(m => m.legacy);
91
+ for (const model of legacyModels) {
92
+ assert.ok(model.name, `Legacy model missing name`);
93
+ assert.ok(model.type, `Legacy model ${model.name} missing type`);
94
+ assert.ok(model.context, `Legacy model ${model.name} missing context`);
95
+ assert.ok(model.price, `Legacy model ${model.name} missing price`);
96
+ assert.ok(model.bestFor, `Legacy model ${model.name} missing bestFor`);
97
+ }
98
+ });
67
99
  });
Binary file