UNPKG

voyageai-cli

Version:

CLI for Voyage AI embeddings, reranking, and MongoDB Atlas Vector Search

245 lines (198 loc) 8.8 kB
'use strict'; const fs = require('fs'); const path = require('path'); const pc = require('picocolors'); const { Optimizer } = require('../lib/optimizer'); const { getConfigValue } = require('../lib/config'); /** * Parse shorthand numbers: "1M" → 1000000 */ function parseShorthand(val) { if (!val) return NaN; const str = String(val).trim().toUpperCase(); const multipliers = { K: 1e3, M: 1e6, B: 1e9, T: 1e12 }; const match = str.match(/^([\d.]+)\s*([KMBT])?$/); if (!match) return parseFloat(str); const num = parseFloat(match[1]); const suffix = match[2]; return suffix ? num * multipliers[suffix] : num; } /** * Format currency */ function formatDollars(n) { return '$' + n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } /** * Generate a Markdown report. */ function generateReport(analysisResult, options = {}) { const { collection, scale } = analysisResult; const { symmetric, asymmetric, savings } = analysisResult.costs; const savingsPercent = ((savings / symmetric) * 100).toFixed(1); let report = `# Voyage AI Cost Optimization Report **Generated by vai** | ${new Date().toISOString().split('T')[0]} | Collection: ${options.collection || 'unknown'} ## Retrieval Quality Compared voyage-4-large (baseline) vs voyage-4-lite (optimized) across ${analysisResult.queries.length} queries. | Metric | Value | |--------|-------| | Average result overlap | ${(analysisResult.queries.reduce((sum, q) => sum + q.overlapPercent, 0) / analysisResult.queries.length).toFixed(1)}% | | Average rank correlation | ${(analysisResult.queries.reduce((sum, q) => sum + q.rankCorrelation, 0) / analysisResult.queries.length).toFixed(3)} | **Conclusion:** voyage-4-lite retrieves nearly identical results from documents embedded with voyage-4-large. Quality degradation is negligible for this dataset. ## Cost Projection **Scale:** ${(scale.docs / 1e6).toFixed(1)}M documents, ${(scale.queriesPerMonth / 1e6).toFixed(1)}M queries/month, ${scale.months} months | Strategy | Annual Cost | Savings | |----------|------------|---------| | Symmetric (large for everything) | ${formatDollars(symmetric)} | — | | Asymmetric (large for docs, lite for queries) | ${formatDollars(asymmetric)} | ${formatDollars(savings)} (${savingsPercent}%) | ## Recommendation Use asymmetric retrieval: embed documents with voyage-4-large for maximum quality, query with voyage-4-lite for minimum cost. At your projected scale, this saves approximately **${formatDollars(savings)}** per year with less than 1% quality degradation. ## Detailed Query Results `; for (let i = 0; i < analysisResult.queries.length; i++) { const q = analysisResult.queries[i]; report += `### Query ${i + 1}: "${q.query}" - Result overlap: ${q.overlap}/5 (${q.overlapPercent.toFixed(1)}%) - Rank correlation: ${q.rankCorrelation.toFixed(3)} `; } report += `--- *Generated by [voyageai-cli](https://github.com/mrlynn/voyageai-cli). Voyage AI provides 200M free tokens to get started.* `; return report; } /** * Register the optimize command. */ function registerOptimize(program) { program .command('optimize') .description('Analyze cost savings with asymmetric retrieval') .option('--db <name>', 'MongoDB database', (val) => getConfigValue('defaultDb') || 'vai_demo') .option('--collection <name>', 'Collection name', (val) => getConfigValue('defaultCollection') || 'knowledge') .option('--queries <text...>', 'Test queries (space-separated)') .option('--models <models...>', 'Models to compare', ['voyage-4-large', 'voyage-4-lite']) .option('--scale <spec>', 'Scale spec: <docs>-docs <queries>-queries <months>-months', '1M-docs 50M-queries 12-months') .option('--export <path>', 'Export report to file (.md, .json)') .option('--json', 'Output raw JSON') .option('-q, --quiet', 'Suppress non-essential output') .action(async (opts) => { try { const telemetry = require('../lib/telemetry'); // Check prerequisites const apiKey = process.env.VOYAGE_API_KEY || getConfigValue('apiKey'); const mongoUri = process.env.MONGODB_URI || getConfigValue('mongodbUri'); if (!apiKey) { console.error(pc.red(' ✗ VOYAGE_API_KEY not configured')); console.error(` ${pc.dim('vai config set api-key "your-key"')}`); process.exit(1); } if (!mongoUri) { console.error(pc.red(' ✗ MONGODB_URI not configured')); console.error(` ${pc.dim('vai config set mongodb-uri "mongodb+srv://..."')}`); process.exit(1); } // Parse scale const scaleMatch = opts.scale.match(/(\d+[KMB]?)-docs\s+(\d+[KMB]?)-queries\s+(\d+)-months/i); if (!scaleMatch) { console.error(pc.red(' Invalid --scale format. Expected: "1M-docs 50M-queries 12-months"')); process.exit(1); } const scale = { docs: parseShorthand(scaleMatch[1]), queriesPerMonth: parseShorthand(scaleMatch[2]), months: parseInt(scaleMatch[3], 10), }; if (!opts.quiet) { console.log(''); console.log(pc.bold(' 💰 Cost Optimizer')); console.log(` Database: ${opts.db}`); console.log(` Collection: ${opts.collection}`); console.log(''); } const optimizer = new Optimizer({ db: opts.db, collection: opts.collection }); // Generate or use provided queries let queries = opts.queries || []; if (queries.length === 0) { if (!opts.quiet) process.stdout.write(' Generating sample queries... '); queries = await optimizer.generateSampleQueries(5); if (!opts.quiet) console.log(pc.green('done')); } // Run analysis if (!opts.quiet) process.stdout.write(' Running analysis... '); const result = await optimizer.analyze({ queries, models: opts.models, scale, }); if (!opts.quiet) console.log(pc.green('done')); console.log(''); // Output results if (opts.json) { console.log(JSON.stringify(result, null, 2)); } else { // Formatted output console.log(pc.cyan(' ── Retrieval Quality ──')); console.log(''); for (let i = 0; i < result.queries.length; i++) { const q = result.queries[i]; const shortQuery = q.query.length > 60 ? q.query.slice(0, 57) + '...' : q.query; console.log(` Query ${i + 1}: "${shortQuery}"`); console.log(` Overlap: ${q.overlap}/5 (${q.overlapPercent.toFixed(1)}%)`); } const avgOverlap = ( result.queries.reduce((sum, q) => sum + q.overlapPercent, 0) / result.queries.length ).toFixed(1); console.log(''); console.log(` Average overlap: ${avgOverlap}%`); console.log(pc.green(' ✓ Results are nearly identical across models')); console.log(''); console.log(pc.cyan(' ── Cost Projection ──')); console.log(''); const { symmetric, asymmetric, savings } = result.costs; const savingsPercent = ((savings / symmetric) * 100).toFixed(1); console.log( ` Symmetric (large everywhere): ${formatDollars(symmetric)}` ); console.log( ` Asymmetric (large→docs, lite→queries): ${formatDollars(asymmetric)}` ); console.log(''); console.log(pc.green(` 💰 Annual savings: ${formatDollars(savings)} (${savingsPercent}%)`)); console.log(''); } // Export if (opts.export) { const ext = path.extname(opts.export).toLowerCase(); let content; if (ext === '.json') { content = JSON.stringify(result, null, 2); } else if (ext === '.md' || !ext) { content = generateReport(result, { collection: opts.collection }); } else { console.error(pc.red(` Unknown format: ${ext}`)); process.exit(1); } fs.writeFileSync(opts.export, content, 'utf-8'); if (!opts.quiet) { console.log(pc.green(` ✓ Report exported to ${opts.export}`)); } } telemetry.send('optimize_completed', { queryCount: result.queries.length, docsScale: scale.docs, queriesPerMonth: scale.queriesPerMonth, }); } catch (err) { console.error(''); console.error(pc.red(' Error:'), err.message); if (process.env.DEBUG) console.error(err); process.exit(1); } }); } module.exports = { registerOptimize };