voyageai-cli
Version:
CLI for Voyage AI embeddings, reranking, and MongoDB Atlas Vector Search
175 lines (152 loc) • 6.8 kB
JavaScript
;
const { MODEL_CATALOG, BENCHMARK_SCORES } = require('../lib/catalog');
const { getApiBase } = require('../lib/api');
const { formatTable } = require('../lib/format');
const ui = require('../lib/ui');
/**
* Shorten dimensions string for compact display.
* "1024 (default), 256, 512, 2048" → "1024*"
* "1024" → "1024"
* "—" → "—"
* @param {string} dims
* @returns {string}
*/
function compactDimensions(dims) {
if (dims === '—') return dims;
const match = dims.match(/^(\d+)\s*\(default\)/);
if (match) return match[1] + '*';
return dims;
}
/**
* Shorten price string for compact display.
* "$0.12/1M tokens" → "$0.12/1M"
* "$0.12/M + $0.60/B px" → "$0.12/M+$0.60/Bpx"
* @param {string} price
* @returns {string}
*/
function compactPrice(price) {
return price.replace('/1M tokens', '/1M').replace(' + ', '+').replace('/B px', '/Bpx');
}
/**
* Register the models command on a Commander program.
* @param {import('commander').Command} program
*/
function registerModels(program) {
program
.command('models')
.description('List available Voyage AI models')
.option('-t, --type <type>', 'Filter by type: embedding, reranking, or all', 'all')
.option('-a, --all', 'Show all models including legacy')
.option('-w, --wide', 'Wide output (show all columns untruncated)')
.option('-b, --benchmarks', 'Show RTEB benchmark scores')
.option('--json', 'Machine-readable JSON output')
.option('-q, --quiet', 'Suppress non-essential output')
.action((opts) => {
const telemetry = require('../lib/telemetry');
telemetry.send('cli_models', { category: opts.type });
let models = MODEL_CATALOG;
// Separate current and legacy models
const showLegacy = opts.all;
const currentModels = models.filter(m => !m.legacy);
const legacyModels = models.filter(m => m.legacy);
if (opts.type !== 'all') {
models = models.filter(m => opts.type === 'embedding' ? m.type.startsWith('embedding') : m.type === opts.type);
}
if (!showLegacy) {
models = models.filter(m => !m.legacy);
}
if (opts.json) {
console.log(JSON.stringify(models, null, 2));
return;
}
if (models.length === 0) {
console.log(ui.yellow(`No models found for type: ${opts.type}`));
return;
}
const apiBase = getApiBase();
if (!opts.quiet) {
console.log(ui.bold('Voyage AI Models'));
console.log(ui.dim(`(via ${apiBase})`));
console.log('');
}
// Split models for display
const displayCurrent = models.filter(m => !m.legacy);
const displayLegacy = models.filter(m => m.legacy);
const formatWideRow = (m) => {
let label = m.unreleased ? ui.cyan(m.name) + ' ' + ui.dim('(unreleased)') : ui.cyan(m.name);
if (m.local) label += ' ' + ui.green('[local]');
if (m.pricePerMToken === 0) label += ' ' + ui.green('[free]');
const type = m.type.startsWith('embedding') ? ui.green(m.type) : ui.yellow(m.type);
const price = ui.dim(m.price);
const arch = m.architecture ? (m.architecture === 'moe' ? ui.cyan('MoE') : m.architecture) : '—';
const space = m.sharedSpace ? ui.green('✓ ' + m.sharedSpace) : '—';
return [label, type, m.context, m.dimensions, arch, space, price, m.bestFor];
};
const formatCompactRow = (m) => {
let label = m.unreleased ? ui.cyan(m.name) + ' ' + ui.dim('(soon)') : ui.cyan(m.name);
if (m.local) label += ' ' + ui.green('[local]');
if (m.pricePerMToken === 0) label += ' ' + ui.green('[free]');
const type = m.type.startsWith('embedding') ? ui.green(m.multimodal ? 'multi' : 'embed') : ui.yellow('rerank');
const dims = compactDimensions(m.dimensions);
const price = ui.dim(compactPrice(m.price));
return [label, type, dims, price, m.shortFor || m.bestFor];
};
if (opts.wide) {
const headers = ['Model', 'Type', 'Context', 'Dimensions', 'Arch', 'Space', 'Price', 'Best For'];
const boldHeaders = headers.map(h => ui.bold(h));
const rows = displayCurrent.map(formatWideRow);
console.log(formatTable(boldHeaders, rows));
if (showLegacy && displayLegacy.length > 0) {
console.log('');
console.log(ui.dim('Legacy Models (use latest for better quality)'));
const legacyRows = displayLegacy.map(formatWideRow);
console.log(formatTable(boldHeaders, legacyRows));
}
} else {
const headers = ['Model', 'Type', 'Dims', 'Price', 'Use Case'];
const boldHeaders = headers.map(h => ui.bold(h));
const rows = displayCurrent.map(formatCompactRow);
console.log(formatTable(boldHeaders, rows));
if (showLegacy && displayLegacy.length > 0) {
console.log('');
console.log(ui.dim('Legacy Models (use latest for better quality)'));
const legacyRows = displayLegacy.map(formatCompactRow);
console.log(formatTable(boldHeaders, legacyRows));
}
}
// Show benchmark scores if requested
if (opts.benchmarks) {
console.log('');
console.log(ui.bold('RTEB Benchmark Scores (NDCG@10, avg 29 datasets)'));
console.log(ui.dim('Source: Voyage AI, January 2026'));
console.log('');
const maxScore = Math.max(...BENCHMARK_SCORES.map(b => b.score));
const barWidth = 30;
for (const b of BENCHMARK_SCORES) {
const barLen = Math.round((b.score / maxScore) * barWidth);
const bar = '█'.repeat(barLen) + '░'.repeat(barWidth - barLen);
const isVoyage = b.provider === 'Voyage AI';
const name = isVoyage ? ui.cyan(b.model.padEnd(22)) : ui.dim(b.model.padEnd(22));
const score = isVoyage ? ui.bold(b.score.toFixed(2)) : b.score.toFixed(2);
const colorBar = isVoyage ? ui.cyan(bar) : ui.dim(bar);
console.log(` ${name} ${colorBar} ${score}`);
}
console.log('');
console.log(ui.dim(' Run "vai explain rteb" for details.'));
}
if (!opts.quiet) {
console.log('');
if (!opts.wide) {
console.log(ui.dim('* = also supports 256, 512, 2048 dimensions'));
}
console.log(ui.dim('Free tier: 200M tokens (most models), 50M (domain-specific)'));
console.log(ui.dim('All 4-series models share the same embedding space.'));
if (!opts.wide && !opts.benchmarks) {
console.log(ui.dim('Use --wide for full details, --benchmarks for RTEB scores.'));
} else if (!opts.wide) {
console.log(ui.dim('Use --wide for full details.'));
}
}
});
}
module.exports = { registerModels };