voyageai-cli 1.12.0 → 1.12.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "voyageai-cli",
3
- "version": "1.12.0",
3
+ "version": "1.12.1",
4
4
  "description": "CLI for Voyage AI embeddings, reranking, and MongoDB Atlas Vector Search",
5
5
  "bin": {
6
6
  "vai": "./src/cli.js"
@@ -40,5 +40,8 @@
40
40
  "ora": "^9.1.0",
41
41
  "picocolors": "^1.1.1",
42
42
  "update-notifier": "^7.3.1"
43
+ },
44
+ "devDependencies": {
45
+ "playwright": "^1.58.1"
43
46
  }
44
47
  }
@@ -53,7 +53,7 @@ function registerModels(program) {
53
53
  const legacyModels = models.filter(m => m.legacy);
54
54
 
55
55
  if (opts.type !== 'all') {
56
- models = models.filter(m => m.type === opts.type);
56
+ models = models.filter(m => opts.type === 'embedding' ? m.type.startsWith('embedding') : m.type === opts.type);
57
57
  }
58
58
 
59
59
  if (!showLegacy) {
@@ -84,14 +84,14 @@ function registerModels(program) {
84
84
 
85
85
  const formatWideRow = (m) => {
86
86
  const name = ui.cyan(m.name);
87
- const type = m.type === 'embedding' ? ui.green(m.type) : ui.yellow(m.type);
87
+ const type = m.type.startsWith('embedding') ? ui.green(m.type) : ui.yellow(m.type);
88
88
  const price = ui.dim(m.price);
89
89
  return [name, type, m.context, m.dimensions, price, m.bestFor];
90
90
  };
91
91
 
92
92
  const formatCompactRow = (m) => {
93
93
  const name = ui.cyan(m.name);
94
- const type = m.type === 'embedding' ? ui.green('embed') : ui.yellow('rerank');
94
+ const type = m.type.startsWith('embedding') ? ui.green(m.multimodal ? 'multi' : 'embed') : ui.yellow('rerank');
95
95
  const dims = compactDimensions(m.dimensions);
96
96
  const price = ui.dim(compactPrice(m.price));
97
97
  return [name, type, dims, price, m.shortFor || m.bestFor];
@@ -13,7 +13,12 @@ function registerPing(program) {
13
13
  .description('Test connectivity to Voyage AI API (and optionally MongoDB)')
14
14
  .option('--json', 'Machine-readable JSON output')
15
15
  .option('-q, --quiet', 'Suppress non-essential output')
16
+ .option('--mask', 'Mask sensitive info (cluster hostnames, endpoints) in output. Also enabled by VAI_MASK=1 env var.')
16
17
  .action(async (opts) => {
18
+ // Support env var so all recordings are masked without remembering the flag
19
+ if (process.env.VAI_MASK === '1' || process.env.VAI_MASK === 'true') {
20
+ opts.mask = true;
21
+ }
17
22
  const results = {};
18
23
 
19
24
  // ── Voyage AI ping ──
@@ -28,6 +33,31 @@ function registerPing(program) {
28
33
  const useColor = !opts.json;
29
34
  const useSpinner = useColor && !opts.quiet;
30
35
 
36
+ // Masking helper: "performance.zbcul.mongodb.net" → "perfo*****.mongodb.net"
37
+ const PUBLIC_HOSTS = ['ai.mongodb.com', 'api.voyageai.com'];
38
+ const maskHost = (host) => {
39
+ if (!opts.mask || !host) return host;
40
+ if (PUBLIC_HOSTS.includes(host)) return host;
41
+ const parts = host.split('.');
42
+ if (parts.length >= 3) {
43
+ const name = parts[0];
44
+ const masked = name.slice(0, Math.min(5, name.length)) + '*****';
45
+ return [masked, ...parts.slice(1)].join('.');
46
+ }
47
+ return host.slice(0, 5) + '*****';
48
+ };
49
+
50
+ const maskUrl = (url) => {
51
+ if (!opts.mask || !url) return url;
52
+ try {
53
+ const u = new URL(url);
54
+ u.hostname = maskHost(u.hostname);
55
+ return u.toString().replace(/\/$/, '');
56
+ } catch {
57
+ return url;
58
+ }
59
+ };
60
+
31
61
  const apiBase = getApiBase();
32
62
  const model = 'voyage-4-lite';
33
63
  const startTime = Date.now();
@@ -94,7 +124,7 @@ function registerPing(program) {
94
124
  console.log(`ok ${elapsed}ms`);
95
125
  } else {
96
126
  console.log(ui.success(`Connected to Voyage AI API ${ui.dim('(' + elapsed + 'ms)')}`));
97
- console.log(ui.label('Endpoint', apiBase));
127
+ console.log(ui.label('Endpoint', maskUrl(apiBase)));
98
128
  console.log(ui.label('Model', model));
99
129
  console.log(ui.label('Dimensions', String(dims)));
100
130
  console.log(ui.label('Tokens', String(tokens)));
@@ -145,7 +175,7 @@ function registerPing(program) {
145
175
  if (!opts.json && !opts.quiet) {
146
176
  console.log('');
147
177
  console.log(ui.success(`Connected to MongoDB Atlas ${ui.dim('(' + mongoElapsed + 'ms)')}`));
148
- console.log(ui.label('Cluster', cluster));
178
+ console.log(ui.label('Cluster', maskHost(cluster)));
149
179
  }
150
180
 
151
181
  await client.close();
@@ -84,7 +84,7 @@ function createPlaygroundServer() {
84
84
 
85
85
  // API: Models
86
86
  if (req.method === 'GET' && req.url === '/api/models') {
87
- const models = MODEL_CATALOG.filter(m => !m.legacy && !m.local);
87
+ const models = MODEL_CATALOG.filter(m => !m.legacy && !m.local && !m.unreleased);
88
88
  res.writeHead(200, { 'Content-Type': 'application/json' });
89
89
  res.end(JSON.stringify({ models }));
90
90
  return;
@@ -16,8 +16,8 @@ function registerSearch(program) {
16
16
  .requiredOption('--query <text>', 'Search query text')
17
17
  .requiredOption('--db <database>', 'Database name')
18
18
  .requiredOption('--collection <name>', 'Collection name')
19
- .requiredOption('--index <name>', 'Vector search index name')
20
- .requiredOption('--field <name>', 'Embedding field name')
19
+ .option('--index <name>', 'Vector search index name', 'vector_index')
20
+ .option('--field <name>', 'Embedding field name', 'embedding')
21
21
  .option('-m, --model <model>', 'Embedding model', getDefaultModel())
22
22
  .option('--input-type <type>', 'Input type for query embedding', 'query')
23
23
  .option('-d, --dimensions <n>', 'Output dimensions', (v) => parseInt(v, 10))
@@ -17,7 +17,7 @@ function registerStore(program) {
17
17
  .description('Embed text and store in MongoDB Atlas')
18
18
  .requiredOption('--db <database>', 'Database name')
19
19
  .requiredOption('--collection <name>', 'Collection name')
20
- .requiredOption('--field <name>', 'Embedding field name')
20
+ .option('--field <name>', 'Embedding field name', 'embedding')
21
21
  .option('--text <text>', 'Text to embed and store')
22
22
  .option('-f, --file <path>', 'File to embed and store (text file or .jsonl for batch mode)')
23
23
  .option('-m, --model <model>', 'Embedding model', getDefaultModel())
package/src/lib/api.js CHANGED
@@ -96,25 +96,29 @@ async function apiRequest(endpoint, body) {
96
96
  } catch {
97
97
  errorDetail = await response.text();
98
98
  }
99
- console.error(`API Error (${response.status}): ${errorDetail}`);
99
+ const errMsg = `API Error (${response.status}): ${errorDetail}`;
100
100
 
101
101
  // Help users diagnose endpoint mismatch
102
+ let hint = '';
102
103
  if (response.status === 403 && base === ATLAS_API_BASE) {
103
- console.error('');
104
- console.error('Hint: 403 on ai.mongodb.com often means your key is for the Voyage AI');
105
- console.error('platform, not MongoDB Atlas. Try switching the base URL:');
106
- console.error('');
107
- console.error(' vai config set base-url https://api.voyageai.com/v1/');
108
- console.error('');
109
- console.error('Or set VOYAGE_API_BASE=https://api.voyageai.com/v1/ in your environment.');
104
+ hint = '\n\nHint: 403 on ai.mongodb.com often means your key is for the Voyage AI' +
105
+ '\nplatform, not MongoDB Atlas. Try switching the base URL:' +
106
+ '\n\n vai config set base-url https://api.voyageai.com/v1/' +
107
+ '\n\nOr set VOYAGE_API_BASE=https://api.voyageai.com/v1/ in your environment.';
110
108
  } else if (response.status === 401 && base === VOYAGE_API_BASE) {
111
- console.error('');
112
- console.error('Hint: 401 on api.voyageai.com may mean your key is an Atlas AI key.');
113
- console.error('Try switching back:');
114
- console.error('');
115
- console.error(' vai config set base-url https://ai.mongodb.com/v1/');
109
+ hint = '\n\nHint: 401 on api.voyageai.com may mean your key is an Atlas AI key.' +
110
+ '\nTry switching back:' +
111
+ '\n\n vai config set base-url https://ai.mongodb.com/v1/';
116
112
  }
117
- process.exit(1);
113
+
114
+ // Log the error + hint to stderr for CLI users
115
+ console.error(errMsg);
116
+ if (hint) console.error(hint);
117
+
118
+ // Throw instead of process.exit so callers (like playground) can catch gracefully
119
+ const err = new Error(errMsg);
120
+ err.statusCode = response.status;
121
+ throw err;
118
122
  }
119
123
 
120
124
  return response.json();
@@ -32,8 +32,8 @@ const MODEL_CATALOG = [
32
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
33
  { name: 'voyage-finance-2', type: 'embedding', context: '32K', dimensions: '1024', price: '$0.12/1M tokens', bestFor: 'Finance', shortFor: 'Finance' },
34
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' },
36
- { 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: '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 },
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
37
  { name: 'rerank-2.5', type: 'reranking', context: '32K', dimensions: '—', price: '$0.05/1M tokens', bestFor: 'Best quality reranking', shortFor: 'Best reranker' },
38
38
  { name: 'rerank-2.5-lite', type: 'reranking', context: '32K', dimensions: '—', price: '$0.02/1M tokens', bestFor: 'Fast reranking', shortFor: 'Fast reranker' },
39
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 },
@@ -42,7 +42,7 @@ const MODEL_CATALOG = [
42
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
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
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', context: '32K', dimensions: '1024', price: '$0.12/1M tokens', bestFor: 'Legacy multimodal', shortFor: 'Legacy multimodal', 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
46
  { name: 'rerank-2', type: 'reranking', context: '16K', dimensions: '—', price: '$0.05/1M tokens', bestFor: 'Legacy reranker', shortFor: 'Legacy reranker', legacy: true },
47
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 },
48
48
  ];
@@ -97,9 +97,8 @@ describe('api', () => {
97
97
 
98
98
  await assert.rejects(
99
99
  () => apiRequest('/embeddings', { input: ['test'], model: 'voyage-4-lite' }),
100
- /process\.exit called/
100
+ /API Error \(400\)/
101
101
  );
102
- assert.equal(exitCode, 1);
103
102
  });
104
103
 
105
104
  it('retries on 429', async () => {