voyageai-cli 1.16.0 → 1.19.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.
@@ -629,16 +629,22 @@ select:focus { outline: none; border-color: var(--accent); }
629
629
  .quant-rank-score { color: var(--text-muted); font-size: 11px; font-family: var(--mono); margin-top: 3px; }
630
630
 
631
631
  /* Cost calculator */
632
+ .cost-controls {
633
+ display: grid;
634
+ grid-template-columns: 1fr 1fr;
635
+ gap: 16px 32px;
636
+ margin-bottom: 20px;
637
+ }
638
+ .cost-controls-full { grid-column: 1 / -1; }
632
639
  .cost-slider-row {
633
640
  display: flex;
634
641
  align-items: center;
635
- gap: 16px;
636
- margin-bottom: 16px;
642
+ gap: 12px;
637
643
  }
638
644
  .cost-slider-label {
639
- font-size: 13px;
645
+ font-size: 12px;
640
646
  color: var(--text-dim);
641
- min-width: 130px;
647
+ min-width: 110px;
642
648
  }
643
649
  .cost-slider {
644
650
  flex: 1;
@@ -659,12 +665,155 @@ select:focus { outline: none; border-color: var(--accent); }
659
665
  }
660
666
  .cost-slider-value {
661
667
  font-family: var(--mono);
662
- font-size: 14px;
668
+ font-size: 13px;
663
669
  color: var(--accent);
664
- min-width: 80px;
670
+ min-width: 70px;
665
671
  text-align: right;
666
672
  font-weight: 600;
667
673
  }
674
+ .cost-mode-toggle {
675
+ display: flex;
676
+ gap: 0;
677
+ border: 1px solid var(--border);
678
+ border-radius: 8px;
679
+ overflow: hidden;
680
+ width: fit-content;
681
+ }
682
+ .cost-mode-btn {
683
+ padding: 8px 20px;
684
+ background: transparent;
685
+ border: none;
686
+ color: var(--text-dim);
687
+ font-size: 13px;
688
+ cursor: pointer;
689
+ transition: all 0.2s;
690
+ font-family: var(--mono);
691
+ }
692
+ .cost-mode-btn.active {
693
+ background: var(--accent);
694
+ color: var(--bg);
695
+ font-weight: 600;
696
+ }
697
+ .cost-mode-btn:hover:not(.active) {
698
+ background: rgba(0, 212, 170, 0.1);
699
+ color: var(--text);
700
+ }
701
+ .cost-select {
702
+ background: var(--bg-input);
703
+ border: 1px solid var(--border);
704
+ border-radius: 6px;
705
+ color: var(--text);
706
+ font-family: var(--mono);
707
+ font-size: 12px;
708
+ padding: 6px 10px;
709
+ min-width: 160px;
710
+ }
711
+ .cost-select:focus { border-color: var(--accent); outline: none; }
712
+ .cost-model-row {
713
+ display: flex;
714
+ align-items: center;
715
+ gap: 12px;
716
+ margin-bottom: 12px;
717
+ }
718
+ .cost-model-label {
719
+ font-size: 12px;
720
+ color: var(--text-dim);
721
+ min-width: 110px;
722
+ }
723
+ .cost-summary {
724
+ display: grid;
725
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
726
+ gap: 12px;
727
+ margin-bottom: 20px;
728
+ }
729
+ .cost-summary-card {
730
+ background: var(--bg-input);
731
+ border-radius: 8px;
732
+ padding: 14px 16px;
733
+ border: 1px solid var(--border);
734
+ }
735
+ .cost-summary-label {
736
+ font-size: 11px;
737
+ color: var(--text-muted);
738
+ text-transform: uppercase;
739
+ letter-spacing: 0.5px;
740
+ margin-bottom: 4px;
741
+ }
742
+ .cost-summary-value {
743
+ font-family: var(--mono);
744
+ font-size: 20px;
745
+ font-weight: 700;
746
+ color: var(--accent);
747
+ }
748
+ .cost-summary-detail {
749
+ font-size: 11px;
750
+ color: var(--text-dim);
751
+ margin-top: 4px;
752
+ font-family: var(--mono);
753
+ }
754
+ .cost-strategy-cards {
755
+ display: grid;
756
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
757
+ gap: 16px;
758
+ margin-top: 16px;
759
+ }
760
+ .cost-strategy {
761
+ background: var(--bg-input);
762
+ border-radius: 10px;
763
+ padding: 18px;
764
+ border: 1px solid var(--border);
765
+ transition: border-color 0.2s, box-shadow 0.2s;
766
+ position: relative;
767
+ }
768
+ .cost-strategy.recommended {
769
+ border-color: var(--accent);
770
+ box-shadow: 0 0 16px var(--accent-glow);
771
+ }
772
+ .cost-strategy-badge {
773
+ position: absolute;
774
+ top: -10px;
775
+ right: 16px;
776
+ background: var(--accent);
777
+ color: var(--bg);
778
+ font-size: 10px;
779
+ font-weight: 700;
780
+ padding: 3px 10px;
781
+ border-radius: 10px;
782
+ text-transform: uppercase;
783
+ letter-spacing: 0.5px;
784
+ }
785
+ .cost-strategy-name {
786
+ font-size: 14px;
787
+ font-weight: 600;
788
+ color: var(--text);
789
+ margin-bottom: 12px;
790
+ }
791
+ .cost-strategy-row {
792
+ display: flex;
793
+ justify-content: space-between;
794
+ align-items: center;
795
+ padding: 4px 0;
796
+ font-size: 12px;
797
+ }
798
+ .cost-strategy-row-label { color: var(--text-dim); }
799
+ .cost-strategy-row-value { font-family: var(--mono); color: var(--text); font-weight: 500; }
800
+ .cost-strategy-total {
801
+ border-top: 1px solid var(--border);
802
+ margin-top: 10px;
803
+ padding-top: 10px;
804
+ display: flex;
805
+ justify-content: space-between;
806
+ align-items: center;
807
+ }
808
+ .cost-strategy-total-label { font-size: 13px; font-weight: 600; color: var(--text); }
809
+ .cost-strategy-total-value { font-family: var(--mono); font-size: 18px; font-weight: 700; color: var(--accent); }
810
+ .cost-savings {
811
+ font-size: 11px;
812
+ color: #4ade80;
813
+ font-weight: 600;
814
+ margin-top: 6px;
815
+ text-align: right;
816
+ }
668
817
  .cost-table {
669
818
  width: 100%;
670
819
  border-collapse: collapse;
@@ -701,6 +850,24 @@ select:focus { outline: none; border-color: var(--accent); }
701
850
  border-radius: 3px;
702
851
  transition: width 0.4s ease;
703
852
  }
853
+ .cost-section-title {
854
+ font-size: 13px;
855
+ font-weight: 600;
856
+ color: var(--text);
857
+ margin: 20px 0 8px;
858
+ display: flex;
859
+ align-items: center;
860
+ gap: 8px;
861
+ }
862
+ .cost-tip {
863
+ font-size: 12px;
864
+ color: var(--text-muted);
865
+ background: rgba(0, 212, 170, 0.05);
866
+ border-left: 3px solid var(--accent);
867
+ padding: 10px 14px;
868
+ border-radius: 0 6px 6px 0;
869
+ margin-top: 16px;
870
+ }
704
871
 
705
872
  /* History chart */
706
873
  .history-empty {
@@ -1184,30 +1351,116 @@ Reranking models rescore initial search results to improve relevance ordering.</
1184
1351
  <!-- ── Cost Panel ── -->
1185
1352
  <div class="bench-view" id="bench-cost">
1186
1353
  <div class="card">
1187
- <div class="card-title">Cost Calculator</div>
1188
- <div class="cost-slider-row">
1189
- <span class="cost-slider-label">Tokens per query</span>
1190
- <input type="range" class="cost-slider" id="costTokens" min="50" max="5000" value="500" step="50">
1191
- <span class="cost-slider-value" id="costTokensValue">500</span>
1354
+ <div class="card-title">💰 RAG Cost Calculator</div>
1355
+
1356
+ <!-- Mode toggle -->
1357
+ <div style="margin-bottom: 20px;">
1358
+ <div class="cost-mode-toggle">
1359
+ <button class="cost-mode-btn active" data-mode="simple" onclick="setCostMode('simple')">Simple</button>
1360
+ <button class="cost-mode-btn" data-mode="rag" onclick="setCostMode('rag')">RAG Planner</button>
1361
+ </div>
1192
1362
  </div>
1193
- <div class="cost-slider-row">
1194
- <span class="cost-slider-label">Queries per day</span>
1195
- <input type="range" class="cost-slider" id="costQueries" min="10" max="500000" value="1000" step="10">
1196
- <span class="cost-slider-value" id="costQueriesValue">1,000</span>
1363
+
1364
+ <!-- Simple mode (query cost comparison) -->
1365
+ <div id="costSimpleMode">
1366
+ <div class="cost-controls cost-controls-full">
1367
+ <div class="cost-slider-row">
1368
+ <span class="cost-slider-label">Tokens / query</span>
1369
+ <input type="range" class="cost-slider" id="costTokens" min="50" max="5000" value="500" step="50">
1370
+ <span class="cost-slider-value" id="costTokensValue">500</span>
1371
+ </div>
1372
+ <div class="cost-slider-row">
1373
+ <span class="cost-slider-label">Queries / day</span>
1374
+ <input type="range" class="cost-slider" id="costQueries" min="10" max="500000" value="1000" step="10">
1375
+ <span class="cost-slider-value" id="costQueriesValue">1,000</span>
1376
+ </div>
1377
+ </div>
1378
+ <table class="cost-table" id="costTable">
1379
+ <thead>
1380
+ <tr>
1381
+ <th>Model</th>
1382
+ <th>Type</th>
1383
+ <th>$/1M tokens</th>
1384
+ <th>Daily Cost</th>
1385
+ <th>Monthly Cost</th>
1386
+ <th style="width:30%">Relative</th>
1387
+ </tr>
1388
+ </thead>
1389
+ <tbody id="costTableBody"></tbody>
1390
+ </table>
1391
+ </div>
1392
+
1393
+ <!-- RAG Planner mode (full TCO) -->
1394
+ <div id="costRagMode" style="display:none;">
1395
+ <div class="cost-section-title">📄 Documents (one-time ingestion)</div>
1396
+ <div class="cost-controls">
1397
+ <div class="cost-slider-row">
1398
+ <span class="cost-slider-label">Documents</span>
1399
+ <input type="range" class="cost-slider" id="ragDocs" min="1000" max="10000000" value="100000" step="1000">
1400
+ <span class="cost-slider-value" id="ragDocsValue">100K</span>
1401
+ </div>
1402
+ <div class="cost-slider-row">
1403
+ <span class="cost-slider-label">Tokens / doc</span>
1404
+ <input type="range" class="cost-slider" id="ragDocTokens" min="50" max="5000" value="500" step="50">
1405
+ <span class="cost-slider-value" id="ragDocTokensValue">500</span>
1406
+ </div>
1407
+ </div>
1408
+
1409
+ <div class="cost-section-title">🔍 Queries (recurring)</div>
1410
+ <div class="cost-controls">
1411
+ <div class="cost-slider-row">
1412
+ <span class="cost-slider-label">Queries / month</span>
1413
+ <input type="range" class="cost-slider" id="ragQueries" min="1000" max="50000000" value="1000000" step="1000">
1414
+ <span class="cost-slider-value" id="ragQueriesValue">1M</span>
1415
+ </div>
1416
+ <div class="cost-slider-row">
1417
+ <span class="cost-slider-label">Tokens / query</span>
1418
+ <input type="range" class="cost-slider" id="ragQueryTokens" min="10" max="500" value="30" step="5">
1419
+ <span class="cost-slider-value" id="ragQueryTokensValue">30</span>
1420
+ </div>
1421
+ </div>
1422
+
1423
+ <div class="cost-section-title">⚙️ Configuration</div>
1424
+ <div class="cost-controls">
1425
+ <div class="cost-model-row">
1426
+ <span class="cost-model-label">Doc model</span>
1427
+ <select class="cost-select" id="ragDocModel"></select>
1428
+ </div>
1429
+ <div class="cost-model-row">
1430
+ <span class="cost-model-label">Query model</span>
1431
+ <select class="cost-select" id="ragQueryModel"></select>
1432
+ </div>
1433
+ <div class="cost-slider-row">
1434
+ <span class="cost-slider-label">Projection</span>
1435
+ <input type="range" class="cost-slider" id="ragMonths" min="1" max="36" value="12" step="1">
1436
+ <span class="cost-slider-value" id="ragMonthsValue">12 mo</span>
1437
+ </div>
1438
+ </div>
1439
+
1440
+ <!-- Summary cards -->
1441
+ <div class="cost-summary" id="ragSummary"></div>
1442
+
1443
+ <!-- Strategy comparison -->
1444
+ <div class="cost-section-title">📊 Strategy Comparison</div>
1445
+ <div class="cost-strategy-cards" id="ragStrategies"></div>
1446
+
1447
+ <!-- Per-model table -->
1448
+ <div class="cost-section-title">📋 Per-Model Breakdown</div>
1449
+ <table class="cost-table" id="ragTable">
1450
+ <thead>
1451
+ <tr>
1452
+ <th>Model</th>
1453
+ <th>Doc Cost</th>
1454
+ <th>Query $/mo</th>
1455
+ <th>Total (projected)</th>
1456
+ <th style="width:25%">Relative</th>
1457
+ </tr>
1458
+ </thead>
1459
+ <tbody id="ragTableBody"></tbody>
1460
+ </table>
1461
+
1462
+ <div class="cost-tip" id="ragTip"></div>
1197
1463
  </div>
1198
- <table class="cost-table" id="costTable">
1199
- <thead>
1200
- <tr>
1201
- <th>Model</th>
1202
- <th>Type</th>
1203
- <th>$/1M tokens</th>
1204
- <th>Daily Cost</th>
1205
- <th>Monthly Cost</th>
1206
- <th style="width:30%">Relative</th>
1207
- </tr>
1208
- </thead>
1209
- <tbody id="costTableBody"></tbody>
1210
- </table>
1211
1464
  </div>
1212
1465
  </div>
1213
1466
 
@@ -2326,7 +2579,49 @@ window.doBenchQuantization = async function() {
2326
2579
  };
2327
2580
 
2328
2581
  // ── Benchmark: Cost Calculator ──
2582
+ // ── Cost Calculator: Shared Helpers ──
2583
+
2584
+ function costFormatDollars(n) {
2585
+ if (n === 0) return '$0.00';
2586
+ if (n < 0.01 && n > 0) return '$' + n.toFixed(4);
2587
+ if (n < 1) return '$' + n.toFixed(2);
2588
+ return '$' + n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
2589
+ }
2590
+
2591
+ function costShortNum(n) {
2592
+ if (n >= 1e9) return (n / 1e9).toFixed(n % 1e9 === 0 ? 0 : 1) + 'B';
2593
+ if (n >= 1e6) return (n / 1e6).toFixed(n % 1e6 === 0 ? 0 : 1) + 'M';
2594
+ if (n >= 1e3) return (n / 1e3).toFixed(n % 1e3 === 0 ? 0 : 1) + 'K';
2595
+ return String(n);
2596
+ }
2597
+
2598
+ function costGetPricePerM(model) {
2599
+ const match = model.price.match(/\$([0-9.]+)\/1M/);
2600
+ return match ? parseFloat(match[1]) : null;
2601
+ }
2602
+
2603
+ function costGetV4Models() {
2604
+ return allModels.filter(m => !m.legacy && !m.unreleased && costGetPricePerM(m) !== null);
2605
+ }
2606
+
2607
+ // ── Cost Mode Toggle ──
2608
+
2609
+ let currentCostMode = 'simple';
2610
+
2611
+ function setCostMode(mode) {
2612
+ currentCostMode = mode;
2613
+ document.querySelectorAll('.cost-mode-btn').forEach(btn => {
2614
+ btn.classList.toggle('active', btn.dataset.mode === mode);
2615
+ });
2616
+ document.getElementById('costSimpleMode').style.display = mode === 'simple' ? '' : 'none';
2617
+ document.getElementById('costRagMode').style.display = mode === 'rag' ? '' : 'none';
2618
+ if (mode === 'rag') updateRagCalculator();
2619
+ }
2620
+
2621
+ // ── Simple Mode (query-only comparison) ──
2622
+
2329
2623
  function initCostCalculator() {
2624
+ // Simple mode sliders
2330
2625
  const tokSlider = document.getElementById('costTokens');
2331
2626
  const qSlider = document.getElementById('costQueries');
2332
2627
  const tokValue = document.getElementById('costTokensValue');
@@ -2342,22 +2637,22 @@ function initCostCalculator() {
2342
2637
 
2343
2638
  tokSlider.addEventListener('input', updateCost);
2344
2639
  qSlider.addEventListener('input', updateCost);
2345
-
2346
- // Initialize
2347
2640
  updateCost();
2641
+
2642
+ // RAG mode init
2643
+ initRagCalculator();
2348
2644
  }
2349
2645
 
2350
2646
  function renderCostTable(tokensPerQuery, queriesPerDay) {
2351
2647
  const tbody = document.getElementById('costTableBody');
2352
2648
  tbody.innerHTML = '';
2353
2649
 
2354
- const models = allModels.filter(m => !m.legacy);
2650
+ const models = allModels.filter(m => !m.legacy && !m.unreleased);
2355
2651
  const rows = [];
2356
2652
 
2357
2653
  models.forEach(m => {
2358
- const match = m.price.match(/\$([0-9.]+)\/1M/);
2359
- if (!match) return;
2360
- const pricePerM = parseFloat(match[1]);
2654
+ const pricePerM = costGetPricePerM(m);
2655
+ if (pricePerM === null) return;
2361
2656
  const dailyTokens = tokensPerQuery * queriesPerDay;
2362
2657
  const dailyCost = (dailyTokens / 1_000_000) * pricePerM;
2363
2658
  const monthlyCost = dailyCost * 30;
@@ -2388,6 +2683,234 @@ function renderCostTable(tokensPerQuery, queriesPerDay) {
2388
2683
  });
2389
2684
  }
2390
2685
 
2686
+ // ── RAG Planner Mode (full TCO with strategies) ──
2687
+
2688
+ function initRagCalculator() {
2689
+ // Populate model dropdowns
2690
+ const embeddingModels = allModels.filter(m =>
2691
+ m.type === 'embedding' && !m.legacy && !m.unreleased && costGetPricePerM(m) !== null
2692
+ );
2693
+
2694
+ const docSelect = document.getElementById('ragDocModel');
2695
+ const querySelect = document.getElementById('ragQueryModel');
2696
+
2697
+ embeddingModels.forEach(m => {
2698
+ const pricePerM = costGetPricePerM(m);
2699
+ const opt1 = document.createElement('option');
2700
+ opt1.value = m.name;
2701
+ opt1.textContent = `${m.name} ($${pricePerM.toFixed(2)}/1M)`;
2702
+ docSelect.appendChild(opt1);
2703
+
2704
+ const opt2 = document.createElement('option');
2705
+ opt2.value = m.name;
2706
+ opt2.textContent = `${m.name} ($${pricePerM.toFixed(2)}/1M)`;
2707
+ querySelect.appendChild(opt2);
2708
+ });
2709
+
2710
+ // Set defaults: voyage-4-large for docs, voyage-4-lite for queries
2711
+ docSelect.value = 'voyage-4-large';
2712
+ querySelect.value = 'voyage-4-lite';
2713
+
2714
+ // Bind all sliders and selects
2715
+ const ids = ['ragDocs', 'ragDocTokens', 'ragQueries', 'ragQueryTokens', 'ragMonths'];
2716
+ ids.forEach(id => {
2717
+ document.getElementById(id).addEventListener('input', updateRagCalculator);
2718
+ });
2719
+ docSelect.addEventListener('change', updateRagCalculator);
2720
+ querySelect.addEventListener('change', updateRagCalculator);
2721
+
2722
+ updateRagCalculator();
2723
+ }
2724
+
2725
+ function updateRagCalculator() {
2726
+ const numDocs = parseInt(document.getElementById('ragDocs').value, 10);
2727
+ const docTokens = parseInt(document.getElementById('ragDocTokens').value, 10);
2728
+ const numQueries = parseInt(document.getElementById('ragQueries').value, 10);
2729
+ const queryTokens = parseInt(document.getElementById('ragQueryTokens').value, 10);
2730
+ const months = parseInt(document.getElementById('ragMonths').value, 10);
2731
+ const docModelName = document.getElementById('ragDocModel').value;
2732
+ const queryModelName = document.getElementById('ragQueryModel').value;
2733
+
2734
+ // Update slider display values
2735
+ document.getElementById('ragDocsValue').textContent = costShortNum(numDocs);
2736
+ document.getElementById('ragDocTokensValue').textContent = docTokens.toLocaleString();
2737
+ document.getElementById('ragQueriesValue').textContent = costShortNum(numQueries);
2738
+ document.getElementById('ragQueryTokensValue').textContent = queryTokens.toLocaleString();
2739
+ document.getElementById('ragMonthsValue').textContent = months + ' mo';
2740
+
2741
+ const docTotalTokens = numDocs * docTokens;
2742
+ const queryTotalTokensPerMonth = numQueries * queryTokens;
2743
+
2744
+ // Get model prices
2745
+ const docModel = allModels.find(m => m.name === docModelName);
2746
+ const queryModel = allModels.find(m => m.name === queryModelName);
2747
+ const docPrice = docModel ? costGetPricePerM(docModel) : 0;
2748
+ const queryPrice = queryModel ? costGetPricePerM(queryModel) : 0;
2749
+
2750
+ // ── Build strategies (same logic as CLI) ──
2751
+ const strategies = [];
2752
+ const v4Embedding = allModels.filter(m =>
2753
+ m.type === 'embedding' && !m.legacy && !m.unreleased &&
2754
+ (m.sharedSpace === 'voyage-4' || m.name.startsWith('voyage-4')) &&
2755
+ costGetPricePerM(m) !== null && costGetPricePerM(m) > 0
2756
+ );
2757
+
2758
+ // Strategy group 1: Symmetric with each V4 model
2759
+ v4Embedding.forEach(m => {
2760
+ const price = costGetPricePerM(m);
2761
+ const docCost = (docTotalTokens / 1e6) * price;
2762
+ const queryCostPerMonth = (queryTotalTokensPerMonth / 1e6) * price;
2763
+ const totalCost = docCost + (queryCostPerMonth * months);
2764
+ strategies.push({
2765
+ name: `Symmetric: ${m.name}`,
2766
+ type: 'symmetric',
2767
+ docModel: m.name,
2768
+ queryModel: m.name,
2769
+ docCost,
2770
+ queryCostPerMonth,
2771
+ totalCost,
2772
+ months,
2773
+ });
2774
+ });
2775
+
2776
+ // Strategy 2: Asymmetric — user-selected combo
2777
+ const asymDocCost = (docTotalTokens / 1e6) * docPrice;
2778
+ const asymQueryCostPerMonth = (queryTotalTokensPerMonth / 1e6) * queryPrice;
2779
+ const asymTotalCost = asymDocCost + (asymQueryCostPerMonth * months);
2780
+
2781
+ // Only add if it's actually asymmetric (different models)
2782
+ if (docModelName !== queryModelName) {
2783
+ strategies.push({
2784
+ name: `Asymmetric: ${docModelName} + ${queryModelName}`,
2785
+ type: 'asymmetric',
2786
+ docModel: docModelName,
2787
+ queryModel: queryModelName,
2788
+ docCost: asymDocCost,
2789
+ queryCostPerMonth: asymQueryCostPerMonth,
2790
+ totalCost: asymTotalCost,
2791
+ months,
2792
+ recommended: true,
2793
+ });
2794
+ }
2795
+
2796
+ // Strategy 3: Asymmetric with nano (local, free queries)
2797
+ strategies.push({
2798
+ name: `Asymmetric: ${docModelName} + nano (local)`,
2799
+ type: 'asymmetric-local',
2800
+ docModel: docModelName,
2801
+ queryModel: 'voyage-4-nano',
2802
+ docCost: asymDocCost,
2803
+ queryCostPerMonth: 0,
2804
+ totalCost: asymDocCost,
2805
+ months,
2806
+ localNote: 'Query cost = $0 (runs locally via HuggingFace)',
2807
+ });
2808
+
2809
+ strategies.sort((a, b) => a.totalCost - b.totalCost);
2810
+ const maxCost = Math.max(...strategies.map(s => s.totalCost), 0.01);
2811
+
2812
+ // ── Render summary cards ──
2813
+ const summaryEl = document.getElementById('ragSummary');
2814
+ summaryEl.innerHTML = `
2815
+ <div class="cost-summary-card">
2816
+ <div class="cost-summary-label">Document tokens</div>
2817
+ <div class="cost-summary-value">${costShortNum(docTotalTokens)}</div>
2818
+ <div class="cost-summary-detail">${costShortNum(numDocs)} docs × ${docTokens.toLocaleString()} tok</div>
2819
+ </div>
2820
+ <div class="cost-summary-card">
2821
+ <div class="cost-summary-label">Query tokens / mo</div>
2822
+ <div class="cost-summary-value">${costShortNum(queryTotalTokensPerMonth)}</div>
2823
+ <div class="cost-summary-detail">${costShortNum(numQueries)} queries × ${queryTokens} tok</div>
2824
+ </div>
2825
+ <div class="cost-summary-card">
2826
+ <div class="cost-summary-label">Best ${months}-mo total</div>
2827
+ <div class="cost-summary-value">${costFormatDollars(strategies[0].totalCost)}</div>
2828
+ <div class="cost-summary-detail">${strategies[0].name}</div>
2829
+ </div>
2830
+ <div class="cost-summary-card">
2831
+ <div class="cost-summary-label">Max potential savings</div>
2832
+ <div class="cost-summary-value" style="color:#4ade80">${maxCost > 0 ? ((1 - strategies[0].totalCost / maxCost) * 100).toFixed(0) + '%' : '0%'}</div>
2833
+ <div class="cost-summary-detail">vs ${strategies[strategies.length - 1].name.split(':')[1]?.trim() || 'most expensive'}</div>
2834
+ </div>
2835
+ `;
2836
+
2837
+ // ── Render strategy cards ──
2838
+ const stratEl = document.getElementById('ragStrategies');
2839
+ stratEl.innerHTML = strategies.map(s => {
2840
+ const savings = maxCost > 0 ? ((1 - s.totalCost / maxCost) * 100) : 0;
2841
+ const savingsHtml = savings > 0 ? `<div class="cost-savings">↓ ${savings.toFixed(0)}% savings</div>` : '';
2842
+ const badgeHtml = s.recommended ? '<div class="cost-strategy-badge">★ Recommended</div>' : '';
2843
+ const localHtml = s.localNote ? `<div style="font-size:11px;color:var(--text-muted);margin-top:6px;">⚡ ${s.localNote}</div>` : '';
2844
+
2845
+ return `
2846
+ <div class="cost-strategy${s.recommended ? ' recommended' : ''}">
2847
+ ${badgeHtml}
2848
+ <div class="cost-strategy-name">${s.name}</div>
2849
+ <div class="cost-strategy-row">
2850
+ <span class="cost-strategy-row-label">Doc embedding</span>
2851
+ <span class="cost-strategy-row-value">${costFormatDollars(s.docCost)} <span style="color:var(--text-muted);font-size:11px">(one-time)</span></span>
2852
+ </div>
2853
+ <div class="cost-strategy-row">
2854
+ <span class="cost-strategy-row-label">Query cost</span>
2855
+ <span class="cost-strategy-row-value">${costFormatDollars(s.queryCostPerMonth)}/mo</span>
2856
+ </div>
2857
+ <div class="cost-strategy-total">
2858
+ <span class="cost-strategy-total-label">${s.months}-month total</span>
2859
+ <span class="cost-strategy-total-value">${costFormatDollars(s.totalCost)}</span>
2860
+ </div>
2861
+ ${savingsHtml}
2862
+ ${localHtml}
2863
+ </div>
2864
+ `;
2865
+ }).join('');
2866
+
2867
+ // ── Render per-model table ──
2868
+ const ragTbody = document.getElementById('ragTableBody');
2869
+ ragTbody.innerHTML = '';
2870
+
2871
+ const allEmbedding = allModels.filter(m =>
2872
+ m.type === 'embedding' && !m.legacy && !m.unreleased && costGetPricePerM(m) !== null && costGetPricePerM(m) > 0
2873
+ );
2874
+
2875
+ const tableRows = allEmbedding.map(m => {
2876
+ const price = costGetPricePerM(m);
2877
+ const dCost = (docTotalTokens / 1e6) * price;
2878
+ const qCostMo = (queryTotalTokensPerMonth / 1e6) * price;
2879
+ const total = dCost + (qCostMo * months);
2880
+ return { name: m.name, dCost, qCostMo, total };
2881
+ }).sort((a, b) => a.total - b.total);
2882
+
2883
+ const maxTable = Math.max(...tableRows.map(r => r.total), 0.01);
2884
+
2885
+ tableRows.forEach(r => {
2886
+ const tr = document.createElement('tr');
2887
+ const barPct = Math.max(2, (r.total / maxTable) * 100);
2888
+ tr.innerHTML = `
2889
+ <td style="color:var(--text)">${r.name}</td>
2890
+ <td>${costFormatDollars(r.dCost)}</td>
2891
+ <td>${costFormatDollars(r.qCostMo)}</td>
2892
+ <td class="cost-highlight">${costFormatDollars(r.total)}</td>
2893
+ <td class="cost-bar-cell" style="position:relative;padding-left:8px;">
2894
+ <div class="cost-bar" style="width:${barPct}%;"></div>
2895
+ <span style="position:relative;z-index:1;font-size:12px;color:var(--text-dim);">${costFormatDollars(r.total)}</span>
2896
+ </td>
2897
+ `;
2898
+ ragTbody.appendChild(tr);
2899
+ });
2900
+
2901
+ // ── Tip ──
2902
+ const bestSym = strategies.find(s => s.type === 'symmetric' && s.docModel === 'voyage-4-large');
2903
+ const bestAsym = strategies.find(s => s.recommended);
2904
+ const tipEl = document.getElementById('ragTip');
2905
+ if (bestSym && bestAsym && bestSym.totalCost > bestAsym.totalCost) {
2906
+ const saved = bestSym.totalCost - bestAsym.totalCost;
2907
+ const pct = ((saved / bestSym.totalCost) * 100).toFixed(0);
2908
+ tipEl.innerHTML = `💡 <strong>Asymmetric retrieval saves ${costFormatDollars(saved)} (${pct}%)</strong> over symmetric voyage-4-large — same document quality, lower query costs. The shared embedding space makes this possible. <code>vai explain shared-space</code>`;
2909
+ } else {
2910
+ tipEl.innerHTML = '💡 Try selecting different doc and query models to see asymmetric cost savings. <code>vai explain shared-space</code>';
2911
+ }
2912
+ }
2913
+
2391
2914
  // ── Benchmark: History ──
2392
2915
  const HISTORY_KEY = 'vai-bench-history';
2393
2916