voyageai-cli 1.13.0 → 1.16.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/package.json +1 -1
- package/src/cli.js +6 -0
- package/src/commands/benchmark.js +164 -0
- package/src/commands/chunk.js +277 -0
- package/src/commands/completions.js +51 -1
- package/src/commands/estimate.js +209 -0
- package/src/commands/init.js +153 -0
- package/src/commands/models.js +32 -4
- package/src/lib/catalog.js +42 -18
- package/src/lib/chunker.js +341 -0
- package/src/lib/explanations.js +183 -0
- package/src/lib/project.js +122 -0
- package/src/lib/readers.js +239 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { MODEL_CATALOG } = require('../lib/catalog');
|
|
4
|
+
const ui = require('../lib/ui');
|
|
5
|
+
|
|
6
|
+
// Average tokens per document/query (rough industry estimates)
|
|
7
|
+
const DEFAULT_DOC_TOKENS = 500;
|
|
8
|
+
const DEFAULT_QUERY_TOKENS = 30;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parse a shorthand number: "1M" → 1000000, "500K" → 500000, "1B" → 1000000000.
|
|
12
|
+
* @param {string} val
|
|
13
|
+
* @returns {number}
|
|
14
|
+
*/
|
|
15
|
+
function parseShorthand(val) {
|
|
16
|
+
if (!val) return NaN;
|
|
17
|
+
const str = String(val).trim().toUpperCase();
|
|
18
|
+
const multipliers = { K: 1e3, M: 1e6, B: 1e9, T: 1e12 };
|
|
19
|
+
const match = str.match(/^([\d.]+)\s*([KMBT])?$/);
|
|
20
|
+
if (!match) return parseFloat(str);
|
|
21
|
+
const num = parseFloat(match[1]);
|
|
22
|
+
const suffix = match[2];
|
|
23
|
+
return suffix ? num * multipliers[suffix] : num;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Format a number with commas: 1234567 → "1,234,567".
|
|
28
|
+
*/
|
|
29
|
+
function formatNum(n) {
|
|
30
|
+
return n.toLocaleString('en-US');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Format dollars: 0.50 → "$0.50", 1234.56 → "$1,234.56".
|
|
35
|
+
*/
|
|
36
|
+
function formatDollars(n) {
|
|
37
|
+
if (n < 0.01 && n > 0) return `$${n.toFixed(4)}`;
|
|
38
|
+
if (n < 1) return `$${n.toFixed(2)}`;
|
|
39
|
+
return '$' + n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Format a large number in short form: 1000000 → "1M".
|
|
44
|
+
*/
|
|
45
|
+
function shortNum(n) {
|
|
46
|
+
if (n >= 1e9) return (n / 1e9).toFixed(n % 1e9 === 0 ? 0 : 1) + 'B';
|
|
47
|
+
if (n >= 1e6) return (n / 1e6).toFixed(n % 1e6 === 0 ? 0 : 1) + 'M';
|
|
48
|
+
if (n >= 1e3) return (n / 1e3).toFixed(n % 1e3 === 0 ? 0 : 1) + 'K';
|
|
49
|
+
return String(n);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Register the estimate command on a Commander program.
|
|
54
|
+
* @param {import('commander').Command} program
|
|
55
|
+
*/
|
|
56
|
+
function registerEstimate(program) {
|
|
57
|
+
program
|
|
58
|
+
.command('estimate')
|
|
59
|
+
.description('Estimate embedding costs — symmetric vs asymmetric strategies')
|
|
60
|
+
.option('--docs <n>', 'Number of documents to embed (supports K/M/B shorthand)', '100K')
|
|
61
|
+
.option('--queries <n>', 'Number of queries per month (supports K/M/B shorthand)', '1M')
|
|
62
|
+
.option('--doc-tokens <n>', 'Average tokens per document', String(DEFAULT_DOC_TOKENS))
|
|
63
|
+
.option('--query-tokens <n>', 'Average tokens per query', String(DEFAULT_QUERY_TOKENS))
|
|
64
|
+
.option('--doc-model <model>', 'Model for document embedding (asymmetric)', 'voyage-4-large')
|
|
65
|
+
.option('--query-model <model>', 'Model for query embedding (asymmetric)', 'voyage-4-lite')
|
|
66
|
+
.option('--months <n>', 'Months to project', '12')
|
|
67
|
+
.option('--json', 'Machine-readable JSON output')
|
|
68
|
+
.option('-q, --quiet', 'Suppress non-essential output')
|
|
69
|
+
.action((opts) => {
|
|
70
|
+
const numDocs = parseShorthand(opts.docs);
|
|
71
|
+
const numQueries = parseShorthand(opts.queries);
|
|
72
|
+
const docTokens = parseInt(opts.docTokens, 10) || DEFAULT_DOC_TOKENS;
|
|
73
|
+
const queryTokens = parseInt(opts.queryTokens, 10) || DEFAULT_QUERY_TOKENS;
|
|
74
|
+
const months = parseInt(opts.months, 10) || 12;
|
|
75
|
+
|
|
76
|
+
if (isNaN(numDocs) || isNaN(numQueries)) {
|
|
77
|
+
console.error(ui.error('Invalid --docs or --queries value. Use numbers or shorthand (e.g., 1M, 500K).'));
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Get model prices
|
|
82
|
+
const v4Models = MODEL_CATALOG.filter(m => m.sharedSpace === 'voyage-4' && m.pricePerMToken != null);
|
|
83
|
+
const docModel = MODEL_CATALOG.find(m => m.name === opts.docModel);
|
|
84
|
+
const queryModel = MODEL_CATALOG.find(m => m.name === opts.queryModel);
|
|
85
|
+
|
|
86
|
+
if (!docModel || docModel.pricePerMToken == null) {
|
|
87
|
+
console.error(ui.error(`Unknown or unpriced model: ${opts.docModel}`));
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
if (!queryModel || queryModel.pricePerMToken == null) {
|
|
91
|
+
console.error(ui.error(`Unknown or unpriced model: ${opts.queryModel}`));
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const docTotalTokens = numDocs * docTokens;
|
|
96
|
+
const queryTotalTokensPerMonth = numQueries * queryTokens;
|
|
97
|
+
|
|
98
|
+
// Calculate costs for different strategies
|
|
99
|
+
const strategies = [];
|
|
100
|
+
|
|
101
|
+
// Strategy 1: Symmetric with each V4 model
|
|
102
|
+
for (const model of v4Models) {
|
|
103
|
+
if (model.pricePerMToken === 0) continue; // skip free models for symmetric
|
|
104
|
+
const docCost = (docTotalTokens / 1e6) * model.pricePerMToken;
|
|
105
|
+
const queryCostPerMonth = (queryTotalTokensPerMonth / 1e6) * model.pricePerMToken;
|
|
106
|
+
const totalCost = docCost + (queryCostPerMonth * months);
|
|
107
|
+
strategies.push({
|
|
108
|
+
name: `Symmetric: ${model.name}`,
|
|
109
|
+
type: 'symmetric',
|
|
110
|
+
docModel: model.name,
|
|
111
|
+
queryModel: model.name,
|
|
112
|
+
docCost,
|
|
113
|
+
queryCostPerMonth,
|
|
114
|
+
totalCost,
|
|
115
|
+
months,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Strategy 2: Asymmetric — user-specified doc+query combo
|
|
120
|
+
const asymDocCost = (docTotalTokens / 1e6) * docModel.pricePerMToken;
|
|
121
|
+
const asymQueryCostPerMonth = (queryTotalTokensPerMonth / 1e6) * queryModel.pricePerMToken;
|
|
122
|
+
const asymTotalCost = asymDocCost + (asymQueryCostPerMonth * months);
|
|
123
|
+
strategies.push({
|
|
124
|
+
name: `Asymmetric: ${docModel.name} docs + ${queryModel.name} queries`,
|
|
125
|
+
type: 'asymmetric',
|
|
126
|
+
docModel: docModel.name,
|
|
127
|
+
queryModel: queryModel.name,
|
|
128
|
+
docCost: asymDocCost,
|
|
129
|
+
queryCostPerMonth: asymQueryCostPerMonth,
|
|
130
|
+
totalCost: asymTotalCost,
|
|
131
|
+
months,
|
|
132
|
+
recommended: true,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Strategy 3: Asymmetric with nano queries (if doc model isn't nano)
|
|
136
|
+
if (opts.queryModel !== 'voyage-4-nano') {
|
|
137
|
+
const nanoModel = MODEL_CATALOG.find(m => m.name === 'voyage-4-nano');
|
|
138
|
+
if (nanoModel) {
|
|
139
|
+
strategies.push({
|
|
140
|
+
name: `Asymmetric: ${docModel.name} docs + voyage-4-nano queries (local)`,
|
|
141
|
+
type: 'asymmetric-local',
|
|
142
|
+
docModel: docModel.name,
|
|
143
|
+
queryModel: 'voyage-4-nano',
|
|
144
|
+
docCost: asymDocCost,
|
|
145
|
+
queryCostPerMonth: 0,
|
|
146
|
+
totalCost: asymDocCost,
|
|
147
|
+
months,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Sort by total cost
|
|
153
|
+
strategies.sort((a, b) => a.totalCost - b.totalCost);
|
|
154
|
+
|
|
155
|
+
if (opts.json) {
|
|
156
|
+
console.log(JSON.stringify({
|
|
157
|
+
params: { docs: numDocs, queries: numQueries, docTokens, queryTokens, months },
|
|
158
|
+
strategies,
|
|
159
|
+
}, null, 2));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Find the most expensive for savings comparison
|
|
164
|
+
const maxCost = Math.max(...strategies.map(s => s.totalCost));
|
|
165
|
+
|
|
166
|
+
if (!opts.quiet) {
|
|
167
|
+
console.log(ui.bold('💰 Voyage AI Cost Estimator'));
|
|
168
|
+
console.log('');
|
|
169
|
+
console.log(ui.label('Documents', `${shortNum(numDocs)} × ${formatNum(docTokens)} tokens = ${shortNum(docTotalTokens)} tokens (one-time)`));
|
|
170
|
+
console.log(ui.label('Queries', `${shortNum(numQueries)}/mo × ${formatNum(queryTokens)} tokens = ${shortNum(queryTotalTokensPerMonth)} tokens/mo`));
|
|
171
|
+
console.log(ui.label('Projection', `${months} months`));
|
|
172
|
+
console.log('');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
console.log(ui.bold('Strategy Comparison:'));
|
|
176
|
+
console.log('');
|
|
177
|
+
|
|
178
|
+
for (const s of strategies) {
|
|
179
|
+
const savings = maxCost > 0 ? ((1 - s.totalCost / maxCost) * 100) : 0;
|
|
180
|
+
const savingsStr = savings > 0 ? ui.green(` (${savings.toFixed(0)}% savings)`) : '';
|
|
181
|
+
const marker = s.recommended ? ui.cyan(' ★ recommended') : '';
|
|
182
|
+
const localNote = s.type === 'asymmetric-local' ? ui.dim(' (query cost = $0, runs locally)') : '';
|
|
183
|
+
|
|
184
|
+
console.log(` ${s.recommended ? ui.cyan('►') : ' '} ${ui.bold(s.name)}${marker}`);
|
|
185
|
+
console.log(` Doc embedding: ${formatDollars(s.docCost)} ${ui.dim('(one-time)')}`);
|
|
186
|
+
console.log(` Query cost: ${formatDollars(s.queryCostPerMonth)}/mo${localNote}`);
|
|
187
|
+
console.log(` ${months}-mo total: ${ui.bold(formatDollars(s.totalCost))}${savingsStr}`);
|
|
188
|
+
console.log('');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Show the asymmetric advantage
|
|
192
|
+
const symmetricLarge = strategies.find(s => s.type === 'symmetric' && s.docModel === 'voyage-4-large');
|
|
193
|
+
const asymmetric = strategies.find(s => s.recommended);
|
|
194
|
+
if (symmetricLarge && asymmetric && symmetricLarge.totalCost > asymmetric.totalCost) {
|
|
195
|
+
const saved = symmetricLarge.totalCost - asymmetric.totalCost;
|
|
196
|
+
const pct = ((saved / symmetricLarge.totalCost) * 100).toFixed(0);
|
|
197
|
+
console.log(ui.success(`Asymmetric retrieval saves ${formatDollars(saved)} (${pct}%) over symmetric voyage-4-large`));
|
|
198
|
+
console.log(ui.dim(' Same document quality — lower query costs. Shared embedding space makes this possible.'));
|
|
199
|
+
console.log('');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (!opts.quiet) {
|
|
203
|
+
console.log(ui.dim('Tip: Use --doc-model and --query-model to compare any combination.'));
|
|
204
|
+
console.log(ui.dim(' Use "vai explain shared-space" to learn about asymmetric retrieval.'));
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
module.exports = { registerEstimate };
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const readline = require('readline');
|
|
6
|
+
const { MODEL_CATALOG } = require('../lib/catalog');
|
|
7
|
+
const { STRATEGIES } = require('../lib/chunker');
|
|
8
|
+
const { defaultProjectConfig, saveProject, findProjectFile, PROJECT_FILE } = require('../lib/project');
|
|
9
|
+
const ui = require('../lib/ui');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Prompt the user for input with a default value.
|
|
13
|
+
* @param {readline.Interface} rl
|
|
14
|
+
* @param {string} question
|
|
15
|
+
* @param {string} [defaultVal]
|
|
16
|
+
* @returns {Promise<string>}
|
|
17
|
+
*/
|
|
18
|
+
function ask(rl, question, defaultVal) {
|
|
19
|
+
const suffix = defaultVal ? ` ${ui.dim(`(${defaultVal})`)}` : '';
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
rl.question(` ${question}${suffix}: `, (answer) => {
|
|
22
|
+
resolve(answer.trim() || defaultVal || '');
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Prompt for a choice from a list.
|
|
29
|
+
* @param {readline.Interface} rl
|
|
30
|
+
* @param {string} question
|
|
31
|
+
* @param {string[]} choices
|
|
32
|
+
* @param {string} defaultVal
|
|
33
|
+
* @returns {Promise<string>}
|
|
34
|
+
*/
|
|
35
|
+
async function askChoice(rl, question, choices, defaultVal) {
|
|
36
|
+
console.log('');
|
|
37
|
+
for (let i = 0; i < choices.length; i++) {
|
|
38
|
+
const marker = choices[i] === defaultVal ? ui.cyan('→') : ' ';
|
|
39
|
+
console.log(` ${marker} ${i + 1}. ${choices[i]}`);
|
|
40
|
+
}
|
|
41
|
+
const answer = await ask(rl, question, defaultVal);
|
|
42
|
+
// Accept number or value
|
|
43
|
+
const num = parseInt(answer, 10);
|
|
44
|
+
if (num >= 1 && num <= choices.length) return choices[num - 1];
|
|
45
|
+
if (choices.includes(answer)) return answer;
|
|
46
|
+
return defaultVal;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Register the init command on a Commander program.
|
|
51
|
+
* @param {import('commander').Command} program
|
|
52
|
+
*/
|
|
53
|
+
function registerInit(program) {
|
|
54
|
+
program
|
|
55
|
+
.command('init')
|
|
56
|
+
.description('Initialize a project with .vai.json configuration')
|
|
57
|
+
.option('-y, --yes', 'Accept all defaults (non-interactive)')
|
|
58
|
+
.option('--force', 'Overwrite existing .vai.json')
|
|
59
|
+
.option('--json', 'Output created config as JSON (non-interactive)')
|
|
60
|
+
.option('-q, --quiet', 'Suppress non-essential output')
|
|
61
|
+
.action(async (opts) => {
|
|
62
|
+
// Check for existing config
|
|
63
|
+
const existing = findProjectFile();
|
|
64
|
+
if (existing && !opts.force) {
|
|
65
|
+
const relPath = path.relative(process.cwd(), existing);
|
|
66
|
+
console.error(ui.warn(`Project already initialized: ${relPath}`));
|
|
67
|
+
console.error(ui.dim(' Use --force to overwrite.'));
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const defaults = defaultProjectConfig();
|
|
72
|
+
|
|
73
|
+
// Non-interactive mode
|
|
74
|
+
if (opts.yes || opts.json) {
|
|
75
|
+
const filePath = saveProject(defaults);
|
|
76
|
+
if (opts.json) {
|
|
77
|
+
console.log(JSON.stringify(defaults, null, 2));
|
|
78
|
+
} else if (!opts.quiet) {
|
|
79
|
+
console.log(ui.success(`Created ${PROJECT_FILE}`));
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Interactive mode
|
|
85
|
+
console.log('');
|
|
86
|
+
console.log(ui.bold(' 🚀 Initialize Voyage AI Project'));
|
|
87
|
+
console.log(ui.dim(' Creates .vai.json in the current directory.'));
|
|
88
|
+
console.log(ui.dim(' Press Enter to accept defaults.'));
|
|
89
|
+
console.log('');
|
|
90
|
+
|
|
91
|
+
const rl = readline.createInterface({
|
|
92
|
+
input: process.stdin,
|
|
93
|
+
output: process.stdout,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
// Embedding model
|
|
98
|
+
const embeddingModels = MODEL_CATALOG
|
|
99
|
+
.filter(m => m.type === 'embedding' && !m.legacy && !m.unreleased)
|
|
100
|
+
.map(m => m.name);
|
|
101
|
+
const model = await askChoice(rl, 'Embedding model', embeddingModels, defaults.model);
|
|
102
|
+
|
|
103
|
+
// MongoDB settings
|
|
104
|
+
console.log('');
|
|
105
|
+
console.log(ui.bold(' MongoDB Atlas'));
|
|
106
|
+
const db = await ask(rl, 'Database name', defaults.db || 'myapp');
|
|
107
|
+
const collection = await ask(rl, 'Collection name', defaults.collection || 'documents');
|
|
108
|
+
const field = await ask(rl, 'Embedding field', defaults.field);
|
|
109
|
+
const index = await ask(rl, 'Vector index name', defaults.index);
|
|
110
|
+
|
|
111
|
+
// Dimensions
|
|
112
|
+
const modelInfo = MODEL_CATALOG.find(m => m.name === model);
|
|
113
|
+
const defaultDims = modelInfo && modelInfo.dimensions.includes('1024') ? '1024' : '512';
|
|
114
|
+
const dimensions = parseInt(await ask(rl, 'Dimensions', defaultDims), 10) || parseInt(defaultDims, 10);
|
|
115
|
+
|
|
116
|
+
// Chunking
|
|
117
|
+
console.log('');
|
|
118
|
+
console.log(ui.bold(' Chunking'));
|
|
119
|
+
const strategy = await askChoice(rl, 'Chunk strategy', STRATEGIES, defaults.chunk.strategy);
|
|
120
|
+
const chunkSize = parseInt(await ask(rl, 'Chunk size (chars)', String(defaults.chunk.size)), 10);
|
|
121
|
+
const chunkOverlap = parseInt(await ask(rl, 'Chunk overlap (chars)', String(defaults.chunk.overlap)), 10);
|
|
122
|
+
|
|
123
|
+
const config = {
|
|
124
|
+
model,
|
|
125
|
+
db,
|
|
126
|
+
collection,
|
|
127
|
+
field,
|
|
128
|
+
inputType: 'document',
|
|
129
|
+
dimensions,
|
|
130
|
+
index,
|
|
131
|
+
chunk: {
|
|
132
|
+
strategy,
|
|
133
|
+
size: chunkSize,
|
|
134
|
+
overlap: chunkOverlap,
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const filePath = saveProject(config);
|
|
139
|
+
console.log('');
|
|
140
|
+
console.log(ui.success(`Created ${path.relative(process.cwd(), filePath)}`));
|
|
141
|
+
console.log('');
|
|
142
|
+
console.log(ui.dim(' Next steps:'));
|
|
143
|
+
console.log(ui.dim(' vai chunk ./docs/ # Chunk your documents'));
|
|
144
|
+
console.log(ui.dim(' vai pipeline ./docs/ # Chunk → embed → store (coming soon)'));
|
|
145
|
+
console.log(ui.dim(' vai search --query "..." # Search your collection'));
|
|
146
|
+
console.log('');
|
|
147
|
+
} finally {
|
|
148
|
+
rl.close();
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
module.exports = { registerInit };
|
package/src/commands/models.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { MODEL_CATALOG } = require('../lib/catalog');
|
|
3
|
+
const { MODEL_CATALOG, BENCHMARK_SCORES } = require('../lib/catalog');
|
|
4
4
|
const { getApiBase } = require('../lib/api');
|
|
5
5
|
const { formatTable } = require('../lib/format');
|
|
6
6
|
const ui = require('../lib/ui');
|
|
@@ -42,6 +42,7 @@ function registerModels(program) {
|
|
|
42
42
|
.option('-t, --type <type>', 'Filter by type: embedding, reranking, or all', 'all')
|
|
43
43
|
.option('-a, --all', 'Show all models including legacy')
|
|
44
44
|
.option('-w, --wide', 'Wide output (show all columns untruncated)')
|
|
45
|
+
.option('-b, --benchmarks', 'Show RTEB benchmark scores')
|
|
45
46
|
.option('--json', 'Machine-readable JSON output')
|
|
46
47
|
.option('-q, --quiet', 'Suppress non-essential output')
|
|
47
48
|
.action((opts) => {
|
|
@@ -86,7 +87,9 @@ function registerModels(program) {
|
|
|
86
87
|
const name = ui.cyan(m.name);
|
|
87
88
|
const type = m.type.startsWith('embedding') ? ui.green(m.type) : ui.yellow(m.type);
|
|
88
89
|
const price = ui.dim(m.price);
|
|
89
|
-
|
|
90
|
+
const arch = m.architecture ? (m.architecture === 'moe' ? ui.cyan('MoE') : m.architecture) : '—';
|
|
91
|
+
const space = m.sharedSpace ? ui.green('✓ ' + m.sharedSpace) : '—';
|
|
92
|
+
return [name, type, m.context, m.dimensions, arch, space, price, m.bestFor];
|
|
90
93
|
};
|
|
91
94
|
|
|
92
95
|
const formatCompactRow = (m) => {
|
|
@@ -98,7 +101,7 @@ function registerModels(program) {
|
|
|
98
101
|
};
|
|
99
102
|
|
|
100
103
|
if (opts.wide) {
|
|
101
|
-
const headers = ['Model', 'Type', 'Context', 'Dimensions', 'Price', 'Best For'];
|
|
104
|
+
const headers = ['Model', 'Type', 'Context', 'Dimensions', 'Arch', 'Space', 'Price', 'Best For'];
|
|
102
105
|
const boldHeaders = headers.map(h => ui.bold(h));
|
|
103
106
|
const rows = displayCurrent.map(formatWideRow);
|
|
104
107
|
console.log(formatTable(boldHeaders, rows));
|
|
@@ -123,6 +126,29 @@ function registerModels(program) {
|
|
|
123
126
|
}
|
|
124
127
|
}
|
|
125
128
|
|
|
129
|
+
// Show benchmark scores if requested
|
|
130
|
+
if (opts.benchmarks) {
|
|
131
|
+
console.log('');
|
|
132
|
+
console.log(ui.bold('RTEB Benchmark Scores (NDCG@10, avg 29 datasets)'));
|
|
133
|
+
console.log(ui.dim('Source: Voyage AI, January 2026'));
|
|
134
|
+
console.log('');
|
|
135
|
+
|
|
136
|
+
const maxScore = Math.max(...BENCHMARK_SCORES.map(b => b.score));
|
|
137
|
+
const barWidth = 30;
|
|
138
|
+
|
|
139
|
+
for (const b of BENCHMARK_SCORES) {
|
|
140
|
+
const barLen = Math.round((b.score / maxScore) * barWidth);
|
|
141
|
+
const bar = '█'.repeat(barLen) + '░'.repeat(barWidth - barLen);
|
|
142
|
+
const isVoyage = b.provider === 'Voyage AI';
|
|
143
|
+
const name = isVoyage ? ui.cyan(b.model.padEnd(22)) : ui.dim(b.model.padEnd(22));
|
|
144
|
+
const score = isVoyage ? ui.bold(b.score.toFixed(2)) : b.score.toFixed(2);
|
|
145
|
+
const colorBar = isVoyage ? ui.cyan(bar) : ui.dim(bar);
|
|
146
|
+
console.log(` ${name} ${colorBar} ${score}`);
|
|
147
|
+
}
|
|
148
|
+
console.log('');
|
|
149
|
+
console.log(ui.dim(' Run "vai explain rteb" for details.'));
|
|
150
|
+
}
|
|
151
|
+
|
|
126
152
|
if (!opts.quiet) {
|
|
127
153
|
console.log('');
|
|
128
154
|
if (!opts.wide) {
|
|
@@ -130,7 +156,9 @@ function registerModels(program) {
|
|
|
130
156
|
}
|
|
131
157
|
console.log(ui.dim('Free tier: 200M tokens (most models), 50M (domain-specific)'));
|
|
132
158
|
console.log(ui.dim('All 4-series models share the same embedding space.'));
|
|
133
|
-
if (!opts.wide) {
|
|
159
|
+
if (!opts.wide && !opts.benchmarks) {
|
|
160
|
+
console.log(ui.dim('Use --wide for full details, --benchmarks for RTEB scores.'));
|
|
161
|
+
} else if (!opts.wide) {
|
|
134
162
|
console.log(ui.dim('Use --wide for full details.'));
|
|
135
163
|
}
|
|
136
164
|
}
|
package/src/lib/catalog.js
CHANGED
|
@@ -24,29 +24,51 @@ function getDefaultDimensions() {
|
|
|
24
24
|
|
|
25
25
|
// The model catalog: like a wine list (I don't drink :-P), except every choice
|
|
26
26
|
// leads to vectors instead of regret.
|
|
27
|
-
/** @type {Array<{name: string, type: string, context: string, dimensions: string, price: string, bestFor: string}>} */
|
|
27
|
+
/** @type {Array<{name: string, type: string, context: string, dimensions: string, price: string, bestFor: string, family?: string, architecture?: string, sharedSpace?: string, huggingface?: string, pricePerMToken?: number, rtebScore?: number}>} */
|
|
28
28
|
const MODEL_CATALOG = [
|
|
29
|
-
{ name: 'voyage-4-large', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.12/1M tokens', bestFor: 'Best quality, multilingual', shortFor: 'Best quality' },
|
|
30
|
-
{ name: 'voyage-4', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.06/1M tokens', bestFor: 'Balanced quality/perf', shortFor: 'Balanced' },
|
|
31
|
-
{ name: 'voyage-4-lite', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.02/1M tokens', bestFor: 'Lowest cost', shortFor: 'Budget' },
|
|
32
|
-
{ name: 'voyage-code-3', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.18/1M tokens', bestFor: 'Code retrieval', shortFor: 'Code' },
|
|
33
|
-
{ name: 'voyage-finance-2', type: 'embedding', context: '32K', dimensions: '1024', price: '$0.12/1M tokens', bestFor: 'Finance', shortFor: 'Finance' },
|
|
34
|
-
{ name: 'voyage-law-2', type: 'embedding', context: '16K', dimensions: '1024', price: '$0.12/1M tokens', bestFor: 'Legal', shortFor: 'Legal' },
|
|
35
|
-
{ name: 'voyage-context-3', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.18/1M tokens', bestFor: 'Contextualized chunks', shortFor: 'Context chunks', unreleased: true },
|
|
29
|
+
{ name: 'voyage-4-large', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.12/1M tokens', pricePerMToken: 0.12, bestFor: 'Best quality, multilingual, MoE', shortFor: 'Best quality', family: 'voyage-4', architecture: 'moe', sharedSpace: 'voyage-4', rtebScore: 71.41 },
|
|
30
|
+
{ name: 'voyage-4', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.06/1M tokens', pricePerMToken: 0.06, bestFor: 'Balanced quality/perf', shortFor: 'Balanced', family: 'voyage-4', architecture: 'dense', sharedSpace: 'voyage-4', rtebScore: 70.07 },
|
|
31
|
+
{ name: 'voyage-4-lite', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.02/1M tokens', pricePerMToken: 0.02, bestFor: 'Lowest cost', shortFor: 'Budget', family: 'voyage-4', architecture: 'dense', sharedSpace: 'voyage-4', rtebScore: 68.10 },
|
|
32
|
+
{ name: 'voyage-code-3', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.18/1M tokens', pricePerMToken: 0.18, bestFor: 'Code retrieval', shortFor: 'Code' },
|
|
33
|
+
{ name: 'voyage-finance-2', type: 'embedding', context: '32K', dimensions: '1024', price: '$0.12/1M tokens', pricePerMToken: 0.12, bestFor: 'Finance', shortFor: 'Finance' },
|
|
34
|
+
{ name: 'voyage-law-2', type: 'embedding', context: '16K', dimensions: '1024', price: '$0.12/1M tokens', pricePerMToken: 0.12, bestFor: 'Legal', shortFor: 'Legal' },
|
|
35
|
+
{ name: 'voyage-context-3', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.18/1M tokens', pricePerMToken: 0.18, bestFor: 'Contextualized chunks', shortFor: 'Context chunks', unreleased: true },
|
|
36
36
|
{ name: 'voyage-multimodal-3.5', type: 'embedding-multimodal', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.12/M + $0.60/B px', bestFor: 'Text + images + video', shortFor: 'Multimodal', multimodal: true },
|
|
37
|
-
{ name: 'rerank-2.5', type: 'reranking', context: '32K', dimensions: '—', price: '$0.05/1M tokens', bestFor: 'Best quality reranking', shortFor: 'Best reranker' },
|
|
38
|
-
{ name: 'rerank-2.5-lite', type: 'reranking', context: '32K', dimensions: '—', price: '$0.02/1M tokens', bestFor: 'Fast reranking', shortFor: 'Fast reranker' },
|
|
39
|
-
{ name: 'voyage-4-nano', type: 'embedding', context: '32K', dimensions: '512 (default), 128, 256', price: 'Open-weight', bestFor: 'Open-weight / edge', shortFor: 'Open / edge', local: true },
|
|
37
|
+
{ name: 'rerank-2.5', type: 'reranking', context: '32K', dimensions: '—', price: '$0.05/1M tokens', pricePerMToken: 0.05, bestFor: 'Best quality reranking', shortFor: 'Best reranker' },
|
|
38
|
+
{ name: 'rerank-2.5-lite', type: 'reranking', context: '32K', dimensions: '—', price: '$0.02/1M tokens', pricePerMToken: 0.02, bestFor: 'Fast reranking', shortFor: 'Fast reranker' },
|
|
39
|
+
{ name: 'voyage-4-nano', type: 'embedding', context: '32K', dimensions: '512 (default), 128, 256', price: 'Open-weight (free)', pricePerMToken: 0, bestFor: 'Open-weight / edge / local', shortFor: 'Open / edge', local: true, family: 'voyage-4', architecture: 'dense', sharedSpace: 'voyage-4', huggingface: 'https://huggingface.co/voyageai/voyage-4-nano', rtebScore: null },
|
|
40
40
|
// Legacy models
|
|
41
|
-
{ 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 },
|
|
42
|
-
{ 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 },
|
|
43
|
-
{ 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 },
|
|
44
|
-
{ name: 'voyage-code-2', type: 'embedding', context: '16K', dimensions: '1536', price: '$0.12/1M tokens', bestFor: 'Legacy code', shortFor: 'Legacy code', legacy: true },
|
|
45
|
-
{ name: 'voyage-multimodal-3', type: 'embedding-multimodal', context: '32K', dimensions: '1024', price: '$0.12/1M tokens', bestFor: 'Legacy multimodal', shortFor: 'Legacy multimodal', legacy: true, multimodal: true },
|
|
46
|
-
{ name: 'rerank-2', type: 'reranking', context: '16K', dimensions: '—', price: '$0.05/1M tokens', bestFor: 'Legacy reranker', shortFor: 'Legacy reranker', legacy: true },
|
|
47
|
-
{ name: 'rerank-2-lite', type: 'reranking', context: '8K', dimensions: '—', price: '$0.02/1M tokens', bestFor: 'Legacy fast reranker', shortFor: 'Legacy fast reranker', legacy: true },
|
|
41
|
+
{ name: 'voyage-3-large', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.18/1M tokens', pricePerMToken: 0.18, bestFor: 'Previous gen quality', shortFor: 'Previous gen quality', legacy: true, rtebScore: null },
|
|
42
|
+
{ name: 'voyage-3.5', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.06/1M tokens', pricePerMToken: 0.06, bestFor: 'Previous gen balanced', shortFor: 'Previous gen balanced', legacy: true, rtebScore: null },
|
|
43
|
+
{ name: 'voyage-3.5-lite', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.02/1M tokens', pricePerMToken: 0.02, bestFor: 'Previous gen budget', shortFor: 'Previous gen budget', legacy: true, rtebScore: null },
|
|
44
|
+
{ name: 'voyage-code-2', type: 'embedding', context: '16K', dimensions: '1536', price: '$0.12/1M tokens', pricePerMToken: 0.12, bestFor: 'Legacy code', shortFor: 'Legacy code', legacy: true },
|
|
45
|
+
{ name: 'voyage-multimodal-3', type: 'embedding-multimodal', context: '32K', dimensions: '1024', price: '$0.12/1M tokens', pricePerMToken: 0.12, bestFor: 'Legacy multimodal', shortFor: 'Legacy multimodal', legacy: true, multimodal: true },
|
|
46
|
+
{ name: 'rerank-2', type: 'reranking', context: '16K', dimensions: '—', price: '$0.05/1M tokens', pricePerMToken: 0.05, bestFor: 'Legacy reranker', shortFor: 'Legacy reranker', legacy: true },
|
|
47
|
+
{ name: 'rerank-2-lite', type: 'reranking', context: '8K', dimensions: '—', price: '$0.02/1M tokens', pricePerMToken: 0.02, bestFor: 'Legacy fast reranker', shortFor: 'Legacy fast reranker', legacy: true },
|
|
48
48
|
];
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* RTEB benchmark scores for competitive models (NDCG@10 average across 29 datasets).
|
|
52
|
+
* Source: Voyage AI blog, January 15 2026.
|
|
53
|
+
*/
|
|
54
|
+
const BENCHMARK_SCORES = [
|
|
55
|
+
{ model: 'voyage-4-large', provider: 'Voyage AI', score: 71.41 },
|
|
56
|
+
{ model: 'voyage-4', provider: 'Voyage AI', score: 70.07 },
|
|
57
|
+
{ model: 'voyage-4-lite', provider: 'Voyage AI', score: 68.10 },
|
|
58
|
+
{ model: 'Gemini Embedding 001', provider: 'Google', score: 68.66 },
|
|
59
|
+
{ model: 'Cohere Embed v4', provider: 'Cohere', score: 65.75 },
|
|
60
|
+
{ model: 'OpenAI v3 Large', provider: 'OpenAI', score: 62.57 },
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get models that share an embedding space.
|
|
65
|
+
* @param {string} space - e.g. 'voyage-4'
|
|
66
|
+
* @returns {Array}
|
|
67
|
+
*/
|
|
68
|
+
function getSharedSpaceModels(space) {
|
|
69
|
+
return MODEL_CATALOG.filter(m => m.sharedSpace === space);
|
|
70
|
+
}
|
|
71
|
+
|
|
50
72
|
module.exports = {
|
|
51
73
|
DEFAULT_EMBED_MODEL,
|
|
52
74
|
DEFAULT_RERANK_MODEL,
|
|
@@ -54,4 +76,6 @@ module.exports = {
|
|
|
54
76
|
getDefaultModel,
|
|
55
77
|
getDefaultDimensions,
|
|
56
78
|
MODEL_CATALOG,
|
|
79
|
+
BENCHMARK_SCORES,
|
|
80
|
+
getSharedSpaceModels,
|
|
57
81
|
};
|