voyageai-cli 1.4.0 → 1.6.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 +38 -0
- package/demo.gif +0 -0
- package/demo.tape +39 -0
- package/package.json +1 -1
- package/scripts/record-demo.sh +63 -0
- package/src/cli.js +2 -0
- package/src/commands/completions.js +463 -0
- package/src/commands/embed.js +16 -2
- package/src/commands/models.js +86 -8
- package/src/commands/ping.js +5 -4
- package/src/commands/rerank.js +12 -2
- package/src/lib/api.js +52 -2
- package/src/lib/catalog.js +19 -10
- package/src/lib/config.js +1 -0
- package/src/lib/explanations.js +11 -8
- package/test/commands/completions.test.js +166 -0
- package/test/commands/embed.test.js +32 -0
- package/test/commands/ingest.test.js +13 -0
- package/test/commands/models.test.js +43 -0
- package/test/commands/ping.test.js +24 -11
- package/test/commands/rerank.test.js +32 -0
- package/test/commands/store.test.js +26 -0
- package/test/lib/api.test.js +12 -3
- package/test/lib/catalog.test.js +32 -0
package/src/commands/models.js
CHANGED
|
@@ -1,10 +1,36 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { MODEL_CATALOG } = require('../lib/catalog');
|
|
4
|
-
const {
|
|
4
|
+
const { getApiBase } = require('../lib/api');
|
|
5
5
|
const { formatTable } = require('../lib/format');
|
|
6
6
|
const ui = require('../lib/ui');
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Shorten dimensions string for compact display.
|
|
10
|
+
* "1024 (default), 256, 512, 2048" → "1024*"
|
|
11
|
+
* "1024" → "1024"
|
|
12
|
+
* "—" → "—"
|
|
13
|
+
* @param {string} dims
|
|
14
|
+
* @returns {string}
|
|
15
|
+
*/
|
|
16
|
+
function compactDimensions(dims) {
|
|
17
|
+
if (dims === '—') return dims;
|
|
18
|
+
const match = dims.match(/^(\d+)\s*\(default\)/);
|
|
19
|
+
if (match) return match[1] + '*';
|
|
20
|
+
return dims;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Shorten price string for compact display.
|
|
25
|
+
* "$0.12/1M tokens" → "$0.12/1M"
|
|
26
|
+
* "$0.12/M + $0.60/B px" → "$0.12/M+$0.60/Bpx"
|
|
27
|
+
* @param {string} price
|
|
28
|
+
* @returns {string}
|
|
29
|
+
*/
|
|
30
|
+
function compactPrice(price) {
|
|
31
|
+
return price.replace('/1M tokens', '/1M').replace(' + ', '+').replace('/B px', '/Bpx');
|
|
32
|
+
}
|
|
33
|
+
|
|
8
34
|
/**
|
|
9
35
|
* Register the models command on a Commander program.
|
|
10
36
|
* @param {import('commander').Command} program
|
|
@@ -14,15 +40,26 @@ function registerModels(program) {
|
|
|
14
40
|
.command('models')
|
|
15
41
|
.description('List available Voyage AI models')
|
|
16
42
|
.option('-t, --type <type>', 'Filter by type: embedding, reranking, or all', 'all')
|
|
43
|
+
.option('-a, --all', 'Show all models including legacy')
|
|
44
|
+
.option('-w, --wide', 'Wide output (show all columns untruncated)')
|
|
17
45
|
.option('--json', 'Machine-readable JSON output')
|
|
18
46
|
.option('-q, --quiet', 'Suppress non-essential output')
|
|
19
47
|
.action((opts) => {
|
|
20
48
|
let models = MODEL_CATALOG;
|
|
21
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
|
+
|
|
22
55
|
if (opts.type !== 'all') {
|
|
23
56
|
models = models.filter(m => m.type === opts.type);
|
|
24
57
|
}
|
|
25
58
|
|
|
59
|
+
if (!showLegacy) {
|
|
60
|
+
models = models.filter(m => !m.legacy);
|
|
61
|
+
}
|
|
62
|
+
|
|
26
63
|
if (opts.json) {
|
|
27
64
|
console.log(JSON.stringify(models, null, 2));
|
|
28
65
|
return;
|
|
@@ -33,28 +70,69 @@ function registerModels(program) {
|
|
|
33
70
|
return;
|
|
34
71
|
}
|
|
35
72
|
|
|
73
|
+
const apiBase = getApiBase();
|
|
74
|
+
|
|
36
75
|
if (!opts.quiet) {
|
|
37
76
|
console.log(ui.bold('Voyage AI Models'));
|
|
38
|
-
console.log(ui.dim(`(via
|
|
77
|
+
console.log(ui.dim(`(via ${apiBase})`));
|
|
39
78
|
console.log('');
|
|
40
79
|
}
|
|
41
80
|
|
|
42
|
-
|
|
43
|
-
const
|
|
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) => {
|
|
44
86
|
const name = ui.cyan(m.name);
|
|
45
87
|
const type = m.type === 'embedding' ? ui.green(m.type) : ui.yellow(m.type);
|
|
46
88
|
const price = ui.dim(m.price);
|
|
47
89
|
return [name, type, m.context, m.dimensions, price, m.bestFor];
|
|
48
|
-
}
|
|
90
|
+
};
|
|
49
91
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
+
|
|
100
|
+
if (opts.wide) {
|
|
101
|
+
const headers = ['Model', 'Type', 'Context', 'Dimensions', 'Price', 'Best For'];
|
|
102
|
+
const boldHeaders = headers.map(h => ui.bold(h));
|
|
103
|
+
const rows = displayCurrent.map(formatWideRow);
|
|
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
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
const headers = ['Model', 'Type', 'Dims', 'Price', 'Use Case'];
|
|
114
|
+
const boldHeaders = headers.map(h => ui.bold(h));
|
|
115
|
+
const rows = displayCurrent.map(formatCompactRow);
|
|
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
|
+
}
|
|
124
|
+
}
|
|
53
125
|
|
|
54
126
|
if (!opts.quiet) {
|
|
55
127
|
console.log('');
|
|
128
|
+
if (!opts.wide) {
|
|
129
|
+
console.log(ui.dim('* = also supports 256, 512, 2048 dimensions'));
|
|
130
|
+
}
|
|
56
131
|
console.log(ui.dim('Free tier: 200M tokens (most models), 50M (domain-specific)'));
|
|
57
132
|
console.log(ui.dim('All 4-series models share the same embedding space.'));
|
|
133
|
+
if (!opts.wide) {
|
|
134
|
+
console.log(ui.dim('Use --wide for full details.'));
|
|
135
|
+
}
|
|
58
136
|
}
|
|
59
137
|
});
|
|
60
138
|
}
|
package/src/commands/ping.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const { getApiBase, requireApiKey } = require('../lib/api');
|
|
4
4
|
const ui = require('../lib/ui');
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -28,6 +28,7 @@ function registerPing(program) {
|
|
|
28
28
|
const useColor = !opts.json;
|
|
29
29
|
const useSpinner = useColor && !opts.quiet;
|
|
30
30
|
|
|
31
|
+
const apiBase = getApiBase();
|
|
31
32
|
const model = 'voyage-4-lite';
|
|
32
33
|
const startTime = Date.now();
|
|
33
34
|
|
|
@@ -38,7 +39,7 @@ function registerPing(program) {
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
try {
|
|
41
|
-
const response = await fetch(`${
|
|
42
|
+
const response = await fetch(`${apiBase}/embeddings`, {
|
|
42
43
|
method: 'POST',
|
|
43
44
|
headers: {
|
|
44
45
|
'Content-Type': 'application/json',
|
|
@@ -83,7 +84,7 @@ function registerPing(program) {
|
|
|
83
84
|
const dims = data.data && data.data[0] ? data.data[0].embedding.length : 'unknown';
|
|
84
85
|
const tokens = data.usage ? data.usage.total_tokens : 'unknown';
|
|
85
86
|
|
|
86
|
-
results.voyage = { ok: true, elapsed, model, dimensions: dims, tokens, endpoint:
|
|
87
|
+
results.voyage = { ok: true, elapsed, model, dimensions: dims, tokens, endpoint: apiBase };
|
|
87
88
|
|
|
88
89
|
if (spin) spin.stop();
|
|
89
90
|
|
|
@@ -93,7 +94,7 @@ function registerPing(program) {
|
|
|
93
94
|
console.log(`ok ${elapsed}ms`);
|
|
94
95
|
} else {
|
|
95
96
|
console.log(ui.success(`Connected to Voyage AI API ${ui.dim('(' + elapsed + 'ms)')}`));
|
|
96
|
-
console.log(ui.label('Endpoint',
|
|
97
|
+
console.log(ui.label('Endpoint', apiBase));
|
|
97
98
|
console.log(ui.label('Model', model));
|
|
98
99
|
console.log(ui.label('Dimensions', String(dims)));
|
|
99
100
|
console.log(ui.label('Tokens', String(tokens)));
|
package/src/commands/rerank.js
CHANGED
|
@@ -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
|
|
110
|
-
const
|
|
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
|
@@ -1,8 +1,32 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const ATLAS_API_BASE = 'https://ai.mongodb.com/v1';
|
|
4
|
+
const VOYAGE_API_BASE = 'https://api.voyageai.com/v1';
|
|
4
5
|
const MAX_RETRIES = 3;
|
|
5
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the API base URL.
|
|
9
|
+
* Priority: VOYAGE_API_BASE env → config baseUrl → auto-detect from key prefix.
|
|
10
|
+
* Keys starting with 'pa-' that work on Voyage platform use VOYAGE_API_BASE.
|
|
11
|
+
* @returns {string}
|
|
12
|
+
*/
|
|
13
|
+
function getApiBase() {
|
|
14
|
+
const { getConfigValue } = require('./config');
|
|
15
|
+
|
|
16
|
+
// Explicit override wins
|
|
17
|
+
const envBase = process.env.VOYAGE_API_BASE;
|
|
18
|
+
if (envBase) return envBase.replace(/\/+$/, '');
|
|
19
|
+
|
|
20
|
+
const configBase = getConfigValue('baseUrl');
|
|
21
|
+
if (configBase) return configBase.replace(/\/+$/, '');
|
|
22
|
+
|
|
23
|
+
// Default to Atlas endpoint
|
|
24
|
+
return ATLAS_API_BASE;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Legacy export for backward compat
|
|
28
|
+
const API_BASE = ATLAS_API_BASE;
|
|
29
|
+
|
|
6
30
|
/**
|
|
7
31
|
* Get the Voyage API key or exit with a helpful error.
|
|
8
32
|
* Checks: env var → config file.
|
|
@@ -18,6 +42,7 @@ function requireApiKey() {
|
|
|
18
42
|
console.error('Option 2: vai config set api-key <your-key>');
|
|
19
43
|
console.error('');
|
|
20
44
|
console.error('Get one from MongoDB Atlas → AI Models → Create model API key');
|
|
45
|
+
console.error(' or Voyage AI platform → Dashboard → API Keys');
|
|
21
46
|
process.exit(1);
|
|
22
47
|
}
|
|
23
48
|
return key;
|
|
@@ -40,7 +65,8 @@ function sleep(ms) {
|
|
|
40
65
|
*/
|
|
41
66
|
async function apiRequest(endpoint, body) {
|
|
42
67
|
const apiKey = requireApiKey();
|
|
43
|
-
const
|
|
68
|
+
const base = getApiBase();
|
|
69
|
+
const url = `${base}${endpoint}`;
|
|
44
70
|
|
|
45
71
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
46
72
|
const response = await fetch(url, {
|
|
@@ -69,6 +95,23 @@ async function apiRequest(endpoint, body) {
|
|
|
69
95
|
errorDetail = await response.text();
|
|
70
96
|
}
|
|
71
97
|
console.error(`API Error (${response.status}): ${errorDetail}`);
|
|
98
|
+
|
|
99
|
+
// Help users diagnose endpoint mismatch
|
|
100
|
+
if (response.status === 403 && base === ATLAS_API_BASE) {
|
|
101
|
+
console.error('');
|
|
102
|
+
console.error('Hint: 403 on ai.mongodb.com often means your key is for the Voyage AI');
|
|
103
|
+
console.error('platform, not MongoDB Atlas. Try switching the base URL:');
|
|
104
|
+
console.error('');
|
|
105
|
+
console.error(' vai config set base-url https://api.voyageai.com/v1/');
|
|
106
|
+
console.error('');
|
|
107
|
+
console.error('Or set VOYAGE_API_BASE=https://api.voyageai.com/v1/ in your environment.');
|
|
108
|
+
} else if (response.status === 401 && base === VOYAGE_API_BASE) {
|
|
109
|
+
console.error('');
|
|
110
|
+
console.error('Hint: 401 on api.voyageai.com may mean your key is an Atlas AI key.');
|
|
111
|
+
console.error('Try switching back:');
|
|
112
|
+
console.error('');
|
|
113
|
+
console.error(' vai config set base-url https://ai.mongodb.com/v1/');
|
|
114
|
+
}
|
|
72
115
|
process.exit(1);
|
|
73
116
|
}
|
|
74
117
|
|
|
@@ -83,6 +126,7 @@ async function apiRequest(endpoint, body) {
|
|
|
83
126
|
* @param {string} [options.model] - Model name
|
|
84
127
|
* @param {string} [options.inputType] - Input type (query|document)
|
|
85
128
|
* @param {number} [options.dimensions] - Output dimensions
|
|
129
|
+
* @param {boolean} [options.truncation] - Enable/disable truncation
|
|
86
130
|
* @returns {Promise<object>} API response with embeddings
|
|
87
131
|
*/
|
|
88
132
|
async function generateEmbeddings(texts, options = {}) {
|
|
@@ -99,12 +143,18 @@ async function generateEmbeddings(texts, options = {}) {
|
|
|
99
143
|
if (options.dimensions) {
|
|
100
144
|
body.output_dimension = options.dimensions;
|
|
101
145
|
}
|
|
146
|
+
if (options.truncation !== undefined) {
|
|
147
|
+
body.truncation = options.truncation;
|
|
148
|
+
}
|
|
102
149
|
|
|
103
150
|
return apiRequest('/embeddings', body);
|
|
104
151
|
}
|
|
105
152
|
|
|
106
153
|
module.exports = {
|
|
107
154
|
API_BASE,
|
|
155
|
+
ATLAS_API_BASE,
|
|
156
|
+
VOYAGE_API_BASE,
|
|
157
|
+
getApiBase,
|
|
108
158
|
requireApiKey,
|
|
109
159
|
apiRequest,
|
|
110
160
|
generateEmbeddings,
|
package/src/lib/catalog.js
CHANGED
|
@@ -24,16 +24,25 @@ function getDefaultDimensions() {
|
|
|
24
24
|
|
|
25
25
|
/** @type {Array<{name: string, type: string, context: string, dimensions: string, price: string, bestFor: string}>} */
|
|
26
26
|
const MODEL_CATALOG = [
|
|
27
|
-
{ name: 'voyage-4-large', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.12/1M tokens', bestFor: 'Best quality, multilingual' },
|
|
28
|
-
{ name: 'voyage-4', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.06/1M tokens', bestFor: 'Balanced quality/perf' },
|
|
29
|
-
{ name: 'voyage-4-lite', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.02/1M tokens', bestFor: 'Lowest cost' },
|
|
30
|
-
{ name: 'voyage-code-3', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.18/1M tokens', bestFor: 'Code retrieval' },
|
|
31
|
-
{ name: 'voyage-finance-2', type: 'embedding', context: '32K', dimensions: '1024', price: '$0.12/1M tokens', bestFor: 'Finance' },
|
|
32
|
-
{ name: 'voyage-law-2', type: 'embedding', context: '16K', dimensions: '1024', price: '$0.12/1M tokens', bestFor: 'Legal' },
|
|
33
|
-
{ name: 'voyage-context-3', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.18/1M tokens', bestFor: 'Contextualized chunks' },
|
|
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' },
|
|
35
|
-
{ name: 'rerank-2.5', type: 'reranking', context: '32K', dimensions: '—', price: '$0.05/1M tokens', bestFor: 'Best quality reranking' },
|
|
36
|
-
{ name: 'rerank-2.5-lite', type: 'reranking', context: '32K', dimensions: '—', price: '$0.02/1M tokens', bestFor: 'Fast reranking' },
|
|
27
|
+
{ 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' },
|
|
28
|
+
{ name: 'voyage-4', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.06/1M tokens', bestFor: 'Balanced quality/perf', shortFor: 'Balanced' },
|
|
29
|
+
{ name: 'voyage-4-lite', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.02/1M tokens', bestFor: 'Lowest cost', shortFor: 'Budget' },
|
|
30
|
+
{ name: 'voyage-code-3', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.18/1M tokens', bestFor: 'Code retrieval', shortFor: 'Code' },
|
|
31
|
+
{ name: 'voyage-finance-2', type: 'embedding', context: '32K', dimensions: '1024', price: '$0.12/1M tokens', bestFor: 'Finance', shortFor: 'Finance' },
|
|
32
|
+
{ name: 'voyage-law-2', type: 'embedding', context: '16K', dimensions: '1024', price: '$0.12/1M tokens', bestFor: 'Legal', shortFor: 'Legal' },
|
|
33
|
+
{ 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' },
|
|
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
|
+
{ name: 'rerank-2.5', type: 'reranking', context: '32K', dimensions: '—', price: '$0.05/1M tokens', bestFor: 'Best quality reranking', shortFor: 'Best reranker' },
|
|
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 = {
|
package/src/lib/config.js
CHANGED
package/src/lib/explanations.js
CHANGED
|
@@ -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('
|
|
214
|
-
`
|
|
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
|
-
|
|
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)')} —
|
|
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')}
|
|
232
|
-
`${pc.cyan('--input-type query')}
|
|
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,166 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { describe, it, beforeEach, afterEach } = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const { Command } = require('commander');
|
|
6
|
+
const { registerCompletions, generateBashCompletions, generateZshCompletions } = require('../../src/commands/completions');
|
|
7
|
+
|
|
8
|
+
describe('completions command', () => {
|
|
9
|
+
let originalLog;
|
|
10
|
+
let originalWrite;
|
|
11
|
+
let originalError;
|
|
12
|
+
let originalExit;
|
|
13
|
+
let output;
|
|
14
|
+
let stdoutOutput;
|
|
15
|
+
let stderrOutput;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
originalLog = console.log;
|
|
19
|
+
originalWrite = process.stdout.write;
|
|
20
|
+
originalError = console.error;
|
|
21
|
+
originalExit = process.exit;
|
|
22
|
+
output = [];
|
|
23
|
+
stdoutOutput = [];
|
|
24
|
+
stderrOutput = [];
|
|
25
|
+
console.log = (...args) => output.push(args.join(' '));
|
|
26
|
+
process.stdout.write = (data) => { stdoutOutput.push(data); return true; };
|
|
27
|
+
console.error = (...args) => stderrOutput.push(args.join(' '));
|
|
28
|
+
process.exit = (code) => { throw new Error(`EXIT_${code}`); };
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
console.log = originalLog;
|
|
33
|
+
process.stdout.write = originalWrite;
|
|
34
|
+
console.error = originalError;
|
|
35
|
+
process.exit = originalExit;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('registers correctly on a program', () => {
|
|
39
|
+
const program = new Command();
|
|
40
|
+
registerCompletions(program);
|
|
41
|
+
const cmd = program.commands.find(c => c.name() === 'completions');
|
|
42
|
+
assert.ok(cmd, 'completions command should be registered');
|
|
43
|
+
assert.ok(cmd.description().includes('completion'), 'should have a description about completions');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('shows usage when called without shell argument', async () => {
|
|
47
|
+
const program = new Command();
|
|
48
|
+
program.exitOverride();
|
|
49
|
+
registerCompletions(program);
|
|
50
|
+
|
|
51
|
+
await program.parseAsync(['node', 'test', 'completions']);
|
|
52
|
+
|
|
53
|
+
const combined = output.join('\n');
|
|
54
|
+
assert.ok(combined.includes('bash'), 'should mention bash');
|
|
55
|
+
assert.ok(combined.includes('zsh'), 'should mention zsh');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('outputs bash completion script', async () => {
|
|
59
|
+
const program = new Command();
|
|
60
|
+
program.exitOverride();
|
|
61
|
+
registerCompletions(program);
|
|
62
|
+
|
|
63
|
+
await program.parseAsync(['node', 'test', 'completions', 'bash']);
|
|
64
|
+
|
|
65
|
+
const combined = stdoutOutput.join('');
|
|
66
|
+
assert.ok(combined.includes('_vai_completions'), 'should contain bash completion function');
|
|
67
|
+
assert.ok(combined.includes('complete -F _vai_completions vai'), 'should register completion');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('outputs zsh completion script', async () => {
|
|
71
|
+
const program = new Command();
|
|
72
|
+
program.exitOverride();
|
|
73
|
+
registerCompletions(program);
|
|
74
|
+
|
|
75
|
+
await program.parseAsync(['node', 'test', 'completions', 'zsh']);
|
|
76
|
+
|
|
77
|
+
const combined = stdoutOutput.join('');
|
|
78
|
+
assert.ok(combined.includes('#compdef vai'), 'should contain zsh compdef header');
|
|
79
|
+
assert.ok(combined.includes('_vai'), 'should contain zsh completion function');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('rejects unknown shell', async () => {
|
|
83
|
+
const program = new Command();
|
|
84
|
+
program.exitOverride();
|
|
85
|
+
registerCompletions(program);
|
|
86
|
+
|
|
87
|
+
await assert.rejects(
|
|
88
|
+
() => program.parseAsync(['node', 'test', 'completions', 'fish']),
|
|
89
|
+
/EXIT_1/,
|
|
90
|
+
'should exit with code 1 for unsupported shell'
|
|
91
|
+
);
|
|
92
|
+
const combined = stderrOutput.join('\n');
|
|
93
|
+
assert.ok(combined.includes('fish'), 'should mention the unknown shell name');
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('generateBashCompletions', () => {
|
|
98
|
+
it('includes all 14 commands (including completions)', () => {
|
|
99
|
+
const script = generateBashCompletions();
|
|
100
|
+
const commands = ['embed', 'rerank', 'store', 'search', 'index', 'models', 'ping', 'config', 'demo', 'explain', 'similarity', 'ingest', 'completions', 'help'];
|
|
101
|
+
for (const cmd of commands) {
|
|
102
|
+
assert.ok(script.includes(cmd), `should include command: ${cmd}`);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('includes model completions', () => {
|
|
107
|
+
const script = generateBashCompletions();
|
|
108
|
+
assert.ok(script.includes('voyage-4-large'), 'should include voyage-4-large model');
|
|
109
|
+
assert.ok(script.includes('rerank-2.5'), 'should include rerank-2.5 model');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('includes flag completions for embed', () => {
|
|
113
|
+
const script = generateBashCompletions();
|
|
114
|
+
assert.ok(script.includes('--model'), 'should include --model flag');
|
|
115
|
+
assert.ok(script.includes('--dimensions'), 'should include --dimensions flag');
|
|
116
|
+
assert.ok(script.includes('--input-type'), 'should include --input-type flag');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('includes index subcommands', () => {
|
|
120
|
+
const script = generateBashCompletions();
|
|
121
|
+
assert.ok(script.includes('create list delete'), 'should include index subcommands');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('includes config subcommands', () => {
|
|
125
|
+
const script = generateBashCompletions();
|
|
126
|
+
assert.ok(script.includes('set get delete path reset'), 'should include config subcommands');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('includes input-type values', () => {
|
|
130
|
+
const script = generateBashCompletions();
|
|
131
|
+
assert.ok(script.includes('query document'), 'should include input-type values');
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('generateZshCompletions', () => {
|
|
136
|
+
it('includes compdef header', () => {
|
|
137
|
+
const script = generateZshCompletions();
|
|
138
|
+
assert.ok(script.startsWith('#compdef vai'), 'should start with #compdef vai');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('includes all commands with descriptions', () => {
|
|
142
|
+
const script = generateZshCompletions();
|
|
143
|
+
const commands = ['embed', 'rerank', 'store', 'search', 'index', 'models', 'ping', 'config', 'demo', 'explain', 'similarity', 'ingest', 'completions'];
|
|
144
|
+
for (const cmd of commands) {
|
|
145
|
+
assert.ok(script.includes(`'${cmd}:`), `should include command with description: ${cmd}`);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('includes model names', () => {
|
|
150
|
+
const script = generateZshCompletions();
|
|
151
|
+
assert.ok(script.includes('voyage-4-large'), 'should include voyage-4-large model');
|
|
152
|
+
assert.ok(script.includes('voyage-code-3'), 'should include voyage-code-3 model');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('includes explain topics', () => {
|
|
156
|
+
const script = generateZshCompletions();
|
|
157
|
+
assert.ok(script.includes('embeddings'), 'should include embeddings topic');
|
|
158
|
+
assert.ok(script.includes('cosine-similarity'), 'should include cosine-similarity topic');
|
|
159
|
+
assert.ok(script.includes('batch-processing'), 'should include batch-processing topic');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('includes file completion for --file flags', () => {
|
|
163
|
+
const script = generateZshCompletions();
|
|
164
|
+
assert.ok(script.includes('_files'), 'should use _files completion');
|
|
165
|
+
});
|
|
166
|
+
});
|
|
@@ -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
|
});
|