voyageai-cli 1.10.0 → 1.12.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 +23 -0
- package/demo.gif +0 -0
- package/package.json +1 -1
- package/src/cli.js +2 -0
- package/src/commands/about.js +85 -0
- package/src/commands/benchmark.js +418 -0
- package/src/commands/embed.js +5 -0
- package/src/commands/playground.js +7 -3
- package/src/commands/store.js +15 -4
- package/src/lib/api.js +6 -0
- package/src/lib/catalog.js +2 -0
- package/src/lib/explanations.js +76 -2
- package/src/lib/math.js +5 -0
- package/src/playground/index.html +530 -1
- package/test/commands/about.test.js +23 -0
- package/test/commands/benchmark.test.js +67 -0
- package/test/commands/embed.test.js +10 -0
- package/test/lib/explanations.test.js +6 -0
- package/voyageai-cli-playground.png +0 -0
|
@@ -551,6 +551,83 @@ select:focus { outline: none; border-color: var(--accent); }
|
|
|
551
551
|
.rank-differ { border-left-color: var(--yellow); }
|
|
552
552
|
.rank-arrow { text-align: center; color: var(--text-muted); font-size: 18px; padding-top: 4px; }
|
|
553
553
|
|
|
554
|
+
/* Quantization charts */
|
|
555
|
+
.quant-charts { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 16px; }
|
|
556
|
+
@media (max-width: 768px) { .quant-charts { grid-template-columns: 1fr; } }
|
|
557
|
+
|
|
558
|
+
.quant-bar-group { margin-bottom: 14px; }
|
|
559
|
+
.quant-bar-label {
|
|
560
|
+
display: flex; justify-content: space-between; align-items: baseline;
|
|
561
|
+
margin-bottom: 4px; font-size: 13px;
|
|
562
|
+
}
|
|
563
|
+
.quant-bar-label .dtype-name { color: var(--accent); font-weight: 600; font-family: var(--mono); }
|
|
564
|
+
.quant-bar-label .dtype-value { color: var(--text-dim); font-family: var(--mono); font-size: 12px; }
|
|
565
|
+
.quant-bar-track {
|
|
566
|
+
height: 32px; background: var(--bg-input); border-radius: 6px;
|
|
567
|
+
overflow: hidden; position: relative;
|
|
568
|
+
}
|
|
569
|
+
.quant-bar-fill {
|
|
570
|
+
height: 100%; border-radius: 6px;
|
|
571
|
+
transition: width 0.8s cubic-bezier(0.22, 1, 0.36, 1);
|
|
572
|
+
display: flex; align-items: center; padding: 0 10px;
|
|
573
|
+
font-family: var(--mono); font-size: 12px; font-weight: 600;
|
|
574
|
+
color: #0a0a1a; white-space: nowrap; min-width: fit-content;
|
|
575
|
+
}
|
|
576
|
+
.quant-bar-fill.storage { background: linear-gradient(90deg, #00d4aa, #4ecdc4); }
|
|
577
|
+
.quant-bar-fill.latency { background: linear-gradient(90deg, #45b7d1, #82aaff); }
|
|
578
|
+
.quant-bar-badge {
|
|
579
|
+
position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
|
|
580
|
+
font-size: 12px; color: var(--text-dim); font-family: var(--mono);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
.quant-quality-meter { margin-bottom: 14px; }
|
|
584
|
+
.quant-meter-header {
|
|
585
|
+
display: flex; justify-content: space-between; align-items: center;
|
|
586
|
+
margin-bottom: 6px;
|
|
587
|
+
}
|
|
588
|
+
.quant-meter-header .dtype-name { color: var(--accent); font-weight: 600; font-family: var(--mono); font-size: 13px; }
|
|
589
|
+
.quant-meter-header .verdict-badge {
|
|
590
|
+
font-size: 12px; padding: 2px 8px; border-radius: 10px; font-weight: 600;
|
|
591
|
+
}
|
|
592
|
+
.quant-meter-header .verdict-badge.perfect { background: rgba(0,212,170,0.15); color: var(--green); }
|
|
593
|
+
.quant-meter-header .verdict-badge.good { background: rgba(255,217,61,0.15); color: var(--yellow); }
|
|
594
|
+
.quant-meter-header .verdict-badge.degraded { background: rgba(255,107,107,0.15); color: var(--red); }
|
|
595
|
+
.quant-meter-track {
|
|
596
|
+
height: 10px; background: var(--bg-input); border-radius: 5px; overflow: hidden;
|
|
597
|
+
}
|
|
598
|
+
.quant-meter-fill {
|
|
599
|
+
height: 100%; border-radius: 5px;
|
|
600
|
+
transition: width 0.8s cubic-bezier(0.22, 1, 0.36, 1);
|
|
601
|
+
}
|
|
602
|
+
.quant-meter-fill.perfect { background: linear-gradient(90deg, #00d4aa, #00e4ba); }
|
|
603
|
+
.quant-meter-fill.good { background: linear-gradient(90deg, #ffd93d, #ffe066); }
|
|
604
|
+
.quant-meter-fill.degraded { background: linear-gradient(90deg, #ff6b6b, #ff8e8e); }
|
|
605
|
+
.quant-meter-detail { font-size: 11px; color: var(--text-muted); margin-top: 4px; font-family: var(--mono); }
|
|
606
|
+
|
|
607
|
+
.quant-rank-cols {
|
|
608
|
+
display: grid; gap: 12px;
|
|
609
|
+
}
|
|
610
|
+
.quant-rank-col-header {
|
|
611
|
+
font-weight: 600; color: var(--accent); font-size: 13px; font-family: var(--mono);
|
|
612
|
+
margin-bottom: 8px; padding-bottom: 6px; border-bottom: 1px solid var(--border);
|
|
613
|
+
}
|
|
614
|
+
.quant-rank-item {
|
|
615
|
+
padding: 8px 10px; margin-bottom: 4px; border-radius: 6px;
|
|
616
|
+
font-size: 12px; position: relative; border-left: 3px solid transparent;
|
|
617
|
+
transition: background 0.2s;
|
|
618
|
+
}
|
|
619
|
+
.quant-rank-item:hover { background: rgba(255,255,255,0.03); }
|
|
620
|
+
.quant-rank-item.match { border-left-color: var(--green); background: rgba(0,212,170,0.06); }
|
|
621
|
+
.quant-rank-item.differ { border-left-color: var(--red); background: rgba(255,107,107,0.06); }
|
|
622
|
+
.quant-rank-item.baseline { border-left-color: var(--border); background: var(--bg-input); }
|
|
623
|
+
.quant-rank-pos {
|
|
624
|
+
display: inline-block; width: 22px; height: 22px; line-height: 22px;
|
|
625
|
+
text-align: center; border-radius: 50%; background: var(--bg-surface);
|
|
626
|
+
color: var(--accent); font-weight: 700; font-size: 11px; font-family: var(--mono);
|
|
627
|
+
margin-right: 8px;
|
|
628
|
+
}
|
|
629
|
+
.quant-rank-score { color: var(--text-muted); font-size: 11px; font-family: var(--mono); margin-top: 3px; }
|
|
630
|
+
|
|
554
631
|
/* Cost calculator */
|
|
555
632
|
.cost-slider-row {
|
|
556
633
|
display: flex;
|
|
@@ -680,6 +757,57 @@ select:focus { outline: none; border-color: var(--accent); }
|
|
|
680
757
|
margin-top: 4px;
|
|
681
758
|
}
|
|
682
759
|
|
|
760
|
+
/* About page */
|
|
761
|
+
.about-container { max-width: 680px; margin: 0 auto; }
|
|
762
|
+
.about-header { display: flex; gap: 24px; align-items: center; margin-bottom: 24px; }
|
|
763
|
+
.about-avatar {
|
|
764
|
+
width: 120px; height: 120px;
|
|
765
|
+
border-radius: 50%;
|
|
766
|
+
border: 3px solid var(--accent);
|
|
767
|
+
box-shadow: 0 0 20px var(--accent-glow);
|
|
768
|
+
flex-shrink: 0;
|
|
769
|
+
}
|
|
770
|
+
.about-name { font-size: 24px; font-weight: 700; color: var(--text); }
|
|
771
|
+
.about-role { font-size: 14px; color: var(--accent); margin-top: 4px; }
|
|
772
|
+
.about-links { display: flex; gap: 12px; margin-top: 8px; }
|
|
773
|
+
.about-links a {
|
|
774
|
+
color: var(--text-dim);
|
|
775
|
+
font-size: 13px;
|
|
776
|
+
text-decoration: none;
|
|
777
|
+
transition: color 0.2s;
|
|
778
|
+
}
|
|
779
|
+
.about-links a:hover { color: var(--accent); }
|
|
780
|
+
.about-section { margin-bottom: 24px; }
|
|
781
|
+
.about-section-title {
|
|
782
|
+
font-size: 13px;
|
|
783
|
+
font-weight: 600;
|
|
784
|
+
color: var(--accent);
|
|
785
|
+
text-transform: uppercase;
|
|
786
|
+
letter-spacing: 0.5px;
|
|
787
|
+
margin-bottom: 8px;
|
|
788
|
+
}
|
|
789
|
+
.about-text { font-size: 14px; line-height: 1.8; color: var(--text); }
|
|
790
|
+
.about-text a { color: var(--accent); text-decoration: none; }
|
|
791
|
+
.about-text a:hover { text-decoration: underline; }
|
|
792
|
+
.about-disclaimer {
|
|
793
|
+
background: rgba(255, 215, 61, 0.08);
|
|
794
|
+
border: 1px solid rgba(255, 215, 61, 0.2);
|
|
795
|
+
border-radius: var(--radius);
|
|
796
|
+
padding: 16px 20px;
|
|
797
|
+
margin-top: 24px;
|
|
798
|
+
}
|
|
799
|
+
.about-disclaimer-title {
|
|
800
|
+
font-size: 13px;
|
|
801
|
+
font-weight: 600;
|
|
802
|
+
color: var(--warning);
|
|
803
|
+
margin-bottom: 6px;
|
|
804
|
+
}
|
|
805
|
+
.about-disclaimer-text {
|
|
806
|
+
font-size: 13px;
|
|
807
|
+
line-height: 1.7;
|
|
808
|
+
color: var(--text-dim);
|
|
809
|
+
}
|
|
810
|
+
|
|
683
811
|
@media (max-width: 768px) {
|
|
684
812
|
.compare-grid, .search-results { grid-template-columns: 1fr; }
|
|
685
813
|
.nav { padding: 0 12px; }
|
|
@@ -711,6 +839,7 @@ select:focus { outline: none; border-color: var(--accent); }
|
|
|
711
839
|
<button class="tab-btn" data-tab="search">🔍 Search</button>
|
|
712
840
|
<button class="tab-btn" data-tab="benchmark">⏱ Benchmark</button>
|
|
713
841
|
<button class="tab-btn" data-tab="explore">📚 Explore</button>
|
|
842
|
+
<button class="tab-btn" data-tab="about">ℹ️ About</button>
|
|
714
843
|
</div>
|
|
715
844
|
|
|
716
845
|
<div class="main">
|
|
@@ -745,6 +874,16 @@ select:focus { outline: none; border-color: var(--accent); }
|
|
|
745
874
|
<option value="2048">2048</option>
|
|
746
875
|
</select>
|
|
747
876
|
</div>
|
|
877
|
+
<div class="option-group">
|
|
878
|
+
<span class="option-label">Output Type</span>
|
|
879
|
+
<select id="embedOutputDtype">
|
|
880
|
+
<option value="float">float (32-bit)</option>
|
|
881
|
+
<option value="int8">int8 (4× smaller)</option>
|
|
882
|
+
<option value="uint8">uint8 (4× smaller)</option>
|
|
883
|
+
<option value="binary">binary (32× smaller)</option>
|
|
884
|
+
<option value="ubinary">ubinary (32× smaller)</option>
|
|
885
|
+
</select>
|
|
886
|
+
</div>
|
|
748
887
|
<button class="btn" id="embedBtn" onclick="doEmbed()">⚡ Embed</button>
|
|
749
888
|
</div>
|
|
750
889
|
|
|
@@ -864,6 +1003,7 @@ Semantic search understands meaning beyond keyword matching</textarea>
|
|
|
864
1003
|
<div class="bench-panels">
|
|
865
1004
|
<button class="bench-panel-btn active" data-bench="latency">⚡ Latency</button>
|
|
866
1005
|
<button class="bench-panel-btn" data-bench="ranking">🏆 Ranking</button>
|
|
1006
|
+
<button class="bench-panel-btn" data-bench="quantization">⚗️ Quantization</button>
|
|
867
1007
|
<button class="bench-panel-btn" data-bench="cost">💰 Cost</button>
|
|
868
1008
|
<button class="bench-panel-btn" data-bench="history">📊 History</button>
|
|
869
1009
|
</div>
|
|
@@ -956,6 +1096,91 @@ Reranking models rescore initial search results to improve relevance ordering.</
|
|
|
956
1096
|
</div>
|
|
957
1097
|
</div>
|
|
958
1098
|
|
|
1099
|
+
<!-- ── Quantization Panel ── -->
|
|
1100
|
+
<div class="bench-view" id="bench-quantization">
|
|
1101
|
+
<div class="card">
|
|
1102
|
+
<div class="card-title">Quantization Benchmark</div>
|
|
1103
|
+
<p style="color:var(--text-dim);font-size:13px;margin-bottom:12px;">
|
|
1104
|
+
Compare how different output data types (float, int8, binary) affect storage size and ranking quality.
|
|
1105
|
+
Embeds the same corpus with each dtype and measures the tradeoff.
|
|
1106
|
+
</p>
|
|
1107
|
+
<div class="options-row" style="flex-wrap:wrap;">
|
|
1108
|
+
<div class="option-group">
|
|
1109
|
+
<span class="option-label">Model</span>
|
|
1110
|
+
<select id="quantModel"></select>
|
|
1111
|
+
</div>
|
|
1112
|
+
<div class="option-group">
|
|
1113
|
+
<span class="option-label">Dimensions</span>
|
|
1114
|
+
<select id="quantDimensions">
|
|
1115
|
+
<option value="">Default</option>
|
|
1116
|
+
<option value="256">256</option>
|
|
1117
|
+
<option value="512">512</option>
|
|
1118
|
+
<option value="1024">1024</option>
|
|
1119
|
+
<option value="2048">2048</option>
|
|
1120
|
+
</select>
|
|
1121
|
+
</div>
|
|
1122
|
+
<div class="option-group">
|
|
1123
|
+
<span class="option-label">Data Types</span>
|
|
1124
|
+
<div id="quantDtypeChecks" style="display:flex;gap:8px;flex-wrap:wrap;">
|
|
1125
|
+
<label style="display:flex;align-items:center;gap:4px;font-size:13px;cursor:pointer;color:var(--text);">
|
|
1126
|
+
<input type="checkbox" value="float" checked style="accent-color:var(--accent);">float
|
|
1127
|
+
</label>
|
|
1128
|
+
<label style="display:flex;align-items:center;gap:4px;font-size:13px;cursor:pointer;color:var(--text);">
|
|
1129
|
+
<input type="checkbox" value="int8" checked style="accent-color:var(--accent);">int8
|
|
1130
|
+
</label>
|
|
1131
|
+
<label style="display:flex;align-items:center;gap:4px;font-size:13px;cursor:pointer;color:var(--text);">
|
|
1132
|
+
<input type="checkbox" value="uint8" style="accent-color:var(--accent);">uint8
|
|
1133
|
+
</label>
|
|
1134
|
+
<label style="display:flex;align-items:center;gap:4px;font-size:13px;cursor:pointer;color:var(--text);">
|
|
1135
|
+
<input type="checkbox" value="ubinary" checked style="accent-color:var(--accent);">ubinary
|
|
1136
|
+
</label>
|
|
1137
|
+
<label style="display:flex;align-items:center;gap:4px;font-size:13px;cursor:pointer;color:var(--text);">
|
|
1138
|
+
<input type="checkbox" value="binary" style="accent-color:var(--accent);">binary
|
|
1139
|
+
</label>
|
|
1140
|
+
</div>
|
|
1141
|
+
</div>
|
|
1142
|
+
</div>
|
|
1143
|
+
<div style="margin-top:12px;">
|
|
1144
|
+
<span class="option-label">Query</span>
|
|
1145
|
+
<input type="text" id="quantQuery" placeholder="Search query..." value="How do I search for similar documents using embeddings?" style="width:100%;margin-bottom:8px;">
|
|
1146
|
+
</div>
|
|
1147
|
+
<div>
|
|
1148
|
+
<span class="option-label">Corpus (one document per line)</span>
|
|
1149
|
+
<textarea id="quantCorpus" rows="5" placeholder="Documents to embed...">Vector search finds documents by computing similarity between embedding vectors in high-dimensional space.
|
|
1150
|
+
MongoDB Atlas Vector Search lets you index and query vector embeddings alongside your operational data.
|
|
1151
|
+
Traditional full-text search uses inverted indexes to match keyword terms in documents.
|
|
1152
|
+
Cosine similarity measures the angle between two vectors, commonly used for semantic search.
|
|
1153
|
+
Database sharding distributes data across multiple servers for horizontal scalability.
|
|
1154
|
+
Embedding models convert text into dense numerical vectors that capture meaning.
|
|
1155
|
+
Approximate nearest neighbor algorithms like HNSW enable fast similarity search at scale.
|
|
1156
|
+
Reranking models rescore initial search results to improve relevance ordering.</textarea>
|
|
1157
|
+
</div>
|
|
1158
|
+
<div style="margin-top:12px;">
|
|
1159
|
+
<button class="btn" id="quantBtn" onclick="doBenchQuantization()">⚗️ Run Quantization Benchmark</button>
|
|
1160
|
+
</div>
|
|
1161
|
+
</div>
|
|
1162
|
+
|
|
1163
|
+
<div class="error-msg" id="quantError"></div>
|
|
1164
|
+
|
|
1165
|
+
<div class="result-section" id="quantResult">
|
|
1166
|
+
<div class="quant-charts">
|
|
1167
|
+
<div class="card">
|
|
1168
|
+
<div class="card-title">📦 Storage per Vector</div>
|
|
1169
|
+
<div id="quantStorageChart"></div>
|
|
1170
|
+
</div>
|
|
1171
|
+
<div class="card">
|
|
1172
|
+
<div class="card-title">⏱ API Latency</div>
|
|
1173
|
+
<div id="quantLatencyChart"></div>
|
|
1174
|
+
</div>
|
|
1175
|
+
</div>
|
|
1176
|
+
<div class="card">
|
|
1177
|
+
<div class="card-title">🎯 Ranking Quality vs Float Baseline</div>
|
|
1178
|
+
<div id="quantQualityMeters" style="margin-bottom:16px;"></div>
|
|
1179
|
+
<div id="quantRankGrid"></div>
|
|
1180
|
+
</div>
|
|
1181
|
+
</div>
|
|
1182
|
+
</div>
|
|
1183
|
+
|
|
959
1184
|
<!-- ── Cost Panel ── -->
|
|
960
1185
|
<div class="bench-view" id="bench-cost">
|
|
961
1186
|
<div class="card">
|
|
@@ -1001,6 +1226,73 @@ Reranking models rescore initial search results to improve relevance ordering.</
|
|
|
1001
1226
|
|
|
1002
1227
|
</div>
|
|
1003
1228
|
|
|
1229
|
+
<!-- ========== ABOUT TAB ========== -->
|
|
1230
|
+
<div class="tab-panel" id="tab-about">
|
|
1231
|
+
<div class="about-container">
|
|
1232
|
+
<div class="card">
|
|
1233
|
+
<div class="about-header">
|
|
1234
|
+
<img src="https://avatars.githubusercontent.com/u/192552?v=4" alt="Michael Lynn" class="about-avatar">
|
|
1235
|
+
<div>
|
|
1236
|
+
<div class="about-name">Michael Lynn</div>
|
|
1237
|
+
<div class="about-role">Principal Staff Developer Advocate · MongoDB</div>
|
|
1238
|
+
<div class="about-links">
|
|
1239
|
+
<a href="https://github.com/mrlynn" target="_blank" rel="noopener">🔗 GitHub</a>
|
|
1240
|
+
<a href="https://mlynn.org" target="_blank" rel="noopener">🌐 mlynn.org</a>
|
|
1241
|
+
<a href="https://www.npmjs.com/package/voyageai-cli" target="_blank" rel="noopener">📦 npm</a>
|
|
1242
|
+
</div>
|
|
1243
|
+
</div>
|
|
1244
|
+
</div>
|
|
1245
|
+
|
|
1246
|
+
<div class="about-section">
|
|
1247
|
+
<div class="about-section-title">About This Project</div>
|
|
1248
|
+
<div class="about-text">
|
|
1249
|
+
<strong>voyageai-cli</strong> (<code style="color:var(--accent);">vai</code>) is a community-built command-line tool for working with
|
|
1250
|
+
<a href="https://www.mongodb.com/docs/voyageai/" target="_blank">Voyage AI</a> embeddings, reranking, and
|
|
1251
|
+
<a href="https://www.mongodb.com/products/platform/atlas-vector-search" target="_blank">MongoDB Atlas Vector Search</a>.
|
|
1252
|
+
It was created to make it easier for developers to explore, benchmark, and integrate
|
|
1253
|
+
Voyage AI models into their applications — right from the terminal or this playground.
|
|
1254
|
+
</div>
|
|
1255
|
+
</div>
|
|
1256
|
+
|
|
1257
|
+
<div class="about-section">
|
|
1258
|
+
<div class="about-section-title">About Michael</div>
|
|
1259
|
+
<div class="about-text">
|
|
1260
|
+
Michael Lynn is a Principal Staff Developer Advocate at MongoDB with 25+ years in enterprise
|
|
1261
|
+
infrastructure and over a decade at MongoDB. He focuses on strategic developer relations,
|
|
1262
|
+
creating educational content around Vector Search, AI enablement, and developer tooling.
|
|
1263
|
+
He builds tools like this to help developers get hands-on with new technology faster.
|
|
1264
|
+
</div>
|
|
1265
|
+
</div>
|
|
1266
|
+
|
|
1267
|
+
<div class="about-section">
|
|
1268
|
+
<div class="about-section-title">What You Can Do Here</div>
|
|
1269
|
+
<div class="about-text">
|
|
1270
|
+
<strong>⚡ Embed</strong> — Generate vector embeddings for any text<br>
|
|
1271
|
+
<strong>⚖️ Compare</strong> — Measure cosine similarity between texts<br>
|
|
1272
|
+
<strong>🔍 Search</strong> — Semantic search with optional reranking<br>
|
|
1273
|
+
<strong>⏱ Benchmark</strong> — Compare model latency, ranking quality, and costs<br>
|
|
1274
|
+
<strong>📚 Explore</strong> — Learn about embeddings, vector search, RAG, and more
|
|
1275
|
+
</div>
|
|
1276
|
+
</div>
|
|
1277
|
+
|
|
1278
|
+
<div class="about-disclaimer">
|
|
1279
|
+
<div class="about-disclaimer-title">⚠️ Community Tool Disclaimer</div>
|
|
1280
|
+
<div class="about-disclaimer-text">
|
|
1281
|
+
This tool is <strong>not</strong> an official product of MongoDB, Inc. or Voyage AI.
|
|
1282
|
+
It is independently built and maintained by Michael Lynn as a community resource.
|
|
1283
|
+
It is not supported, endorsed, or guaranteed by either company. Use at your own discretion.
|
|
1284
|
+
For official documentation, visit
|
|
1285
|
+
<a href="https://www.mongodb.com/docs/voyageai/" target="_blank" style="color:var(--warning);">mongodb.com/docs/voyageai</a>.
|
|
1286
|
+
</div>
|
|
1287
|
+
</div>
|
|
1288
|
+
</div>
|
|
1289
|
+
|
|
1290
|
+
<div style="text-align:center;margin-top:16px;font-size:12px;color:var(--text-muted);">
|
|
1291
|
+
Made with ☕ and curiosity · <a href="https://github.com/mrlynn/voyageai-cli" target="_blank" style="color:var(--text-dim);">Source on GitHub</a>
|
|
1292
|
+
</div>
|
|
1293
|
+
</div>
|
|
1294
|
+
</div>
|
|
1295
|
+
|
|
1004
1296
|
<!-- ========== EXPLORE TAB ========== -->
|
|
1005
1297
|
<div class="tab-panel" id="tab-explore">
|
|
1006
1298
|
<div style="margin-bottom:16px;">
|
|
@@ -1119,6 +1411,12 @@ function populateModelSelects() {
|
|
|
1119
1411
|
}
|
|
1120
1412
|
|
|
1121
1413
|
// ── API Helpers ──
|
|
1414
|
+
function formatBytesUI(bytes) {
|
|
1415
|
+
if (bytes >= 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
1416
|
+
if (bytes >= 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
1417
|
+
return bytes + ' B';
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1122
1420
|
async function apiPost(url, body) {
|
|
1123
1421
|
const res = await fetch(url, {
|
|
1124
1422
|
method: 'POST',
|
|
@@ -1165,16 +1463,29 @@ window.doEmbed = async function() {
|
|
|
1165
1463
|
const dims = document.getElementById('embedDimensions').value;
|
|
1166
1464
|
const dimensions = dims ? parseInt(dims, 10) : undefined;
|
|
1167
1465
|
|
|
1168
|
-
const
|
|
1466
|
+
const outputDtype = document.getElementById('embedOutputDtype').value;
|
|
1467
|
+
const body = { texts: [text], model, inputType, dimensions };
|
|
1468
|
+
if (outputDtype && outputDtype !== 'float') body.output_dtype = outputDtype;
|
|
1469
|
+
|
|
1470
|
+
const data = await apiPost('/api/embed', body);
|
|
1169
1471
|
const emb = data.data[0].embedding;
|
|
1170
1472
|
lastEmbedding = emb;
|
|
1171
1473
|
|
|
1172
1474
|
// Stats
|
|
1475
|
+
const dtype = outputDtype || 'float';
|
|
1476
|
+
const bytesPerDim = (dtype === 'binary' || dtype === 'ubinary') ? 0.125 : (dtype === 'int8' || dtype === 'uint8') ? 1 : 4;
|
|
1477
|
+
const totalBytes = emb.length * bytesPerDim;
|
|
1478
|
+
const storageLine = dtype !== 'float'
|
|
1479
|
+
? `<br><span style="color:var(--success)">📦 ${dtype}: ${formatBytesUI(totalBytes)}/vector (${(4 * emb.length / totalBytes).toFixed(0)}× smaller than float)</span>`
|
|
1480
|
+
: '';
|
|
1481
|
+
|
|
1173
1482
|
const statsEl = document.getElementById('embedStats');
|
|
1174
1483
|
statsEl.innerHTML = `
|
|
1175
1484
|
<span class="stat"><span class="stat-label">Model</span><span class="stat-value">${data.model}</span></span>
|
|
1176
1485
|
<span class="stat"><span class="stat-label">Dimensions</span><span class="stat-value">${emb.length}</span></span>
|
|
1177
1486
|
<span class="stat"><span class="stat-label">Tokens</span><span class="stat-value">${data.usage?.total_tokens || '—'}</span></span>
|
|
1487
|
+
<span class="stat"><span class="stat-label">Type</span><span class="stat-value">${dtype}</span></span>
|
|
1488
|
+
${storageLine}
|
|
1178
1489
|
`;
|
|
1179
1490
|
|
|
1180
1491
|
// Vector preview
|
|
@@ -1410,6 +1721,7 @@ const CONCEPT_META = {
|
|
|
1410
1721
|
'api-access': { icon: '🌐', tab: 'embed' },
|
|
1411
1722
|
'batch-processing': { icon: '📦', tab: 'embed' },
|
|
1412
1723
|
benchmarking: { icon: '⏱', tab: 'benchmark' },
|
|
1724
|
+
quantization: { icon: '⚗️', tab: 'benchmark' },
|
|
1413
1725
|
};
|
|
1414
1726
|
|
|
1415
1727
|
let exploreConcepts = {};
|
|
@@ -1797,6 +2109,222 @@ function renderRankComparison(modelA, modelB, rankedA, rankedB, topK) {
|
|
|
1797
2109
|
}
|
|
1798
2110
|
}
|
|
1799
2111
|
|
|
2112
|
+
// ── Benchmark: Quantization ──
|
|
2113
|
+
function populateQuantModelSelect() {
|
|
2114
|
+
const sel = document.getElementById('quantModel');
|
|
2115
|
+
sel.innerHTML = '';
|
|
2116
|
+
embedModels.forEach(m => {
|
|
2117
|
+
const opt = document.createElement('option');
|
|
2118
|
+
opt.value = m.name;
|
|
2119
|
+
opt.textContent = m.name;
|
|
2120
|
+
sel.appendChild(opt);
|
|
2121
|
+
});
|
|
2122
|
+
// Default to voyage-4-large if available
|
|
2123
|
+
const preferred = embedModels.find(m => m.name === 'voyage-4-large');
|
|
2124
|
+
if (preferred) sel.value = preferred.name;
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
function hammingSimUI(a, b) {
|
|
2128
|
+
// For binary/ubinary packed embeddings, compute agreement via dot product
|
|
2129
|
+
let dot = 0;
|
|
2130
|
+
for (let i = 0; i < a.length; i++) dot += a[i] * b[i];
|
|
2131
|
+
return dot;
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
window.doBenchQuantization = async function() {
|
|
2135
|
+
hideError('quantError');
|
|
2136
|
+
const model = document.getElementById('quantModel').value;
|
|
2137
|
+
const dimsVal = document.getElementById('quantDimensions').value;
|
|
2138
|
+
const dimensions = dimsVal ? parseInt(dimsVal, 10) : undefined;
|
|
2139
|
+
const query = document.getElementById('quantQuery').value.trim();
|
|
2140
|
+
const corpusText = document.getElementById('quantCorpus').value.trim();
|
|
2141
|
+
|
|
2142
|
+
if (!query) { showError('quantError', 'Enter a query'); return; }
|
|
2143
|
+
if (!corpusText) { showError('quantError', 'Enter at least 2 documents'); return; }
|
|
2144
|
+
|
|
2145
|
+
const corpus = corpusText.split('\n').map(d => d.trim()).filter(Boolean);
|
|
2146
|
+
if (corpus.length < 2) { showError('quantError', 'Enter at least 2 documents'); return; }
|
|
2147
|
+
|
|
2148
|
+
const checks = document.querySelectorAll('#quantDtypeChecks input:checked');
|
|
2149
|
+
const dtypes = Array.from(checks).map(c => c.value);
|
|
2150
|
+
if (dtypes.length === 0) { showError('quantError', 'Select at least one data type'); return; }
|
|
2151
|
+
|
|
2152
|
+
setLoading('quantBtn', true);
|
|
2153
|
+
|
|
2154
|
+
try {
|
|
2155
|
+
const allTexts = [query, ...corpus];
|
|
2156
|
+
const resultsByDtype = {};
|
|
2157
|
+
|
|
2158
|
+
for (const dtype of dtypes) {
|
|
2159
|
+
const body = { texts: allTexts, model, inputType: 'document' };
|
|
2160
|
+
if (dimensions) body.dimensions = dimensions;
|
|
2161
|
+
if (dtype !== 'float') body.output_dtype = dtype;
|
|
2162
|
+
|
|
2163
|
+
const start = performance.now();
|
|
2164
|
+
const data = await apiPost('/api/embed', body);
|
|
2165
|
+
const elapsed = performance.now() - start;
|
|
2166
|
+
|
|
2167
|
+
const embeddings = data.data.map(d => d.embedding);
|
|
2168
|
+
const queryEmbed = embeddings[0];
|
|
2169
|
+
const dims = embeddings[0].length;
|
|
2170
|
+
const isBinary = (dtype === 'binary' || dtype === 'ubinary');
|
|
2171
|
+
|
|
2172
|
+
// Rank corpus documents by similarity
|
|
2173
|
+
const ranked = corpus.map((text, i) => {
|
|
2174
|
+
const docEmbed = embeddings[i + 1];
|
|
2175
|
+
let sim;
|
|
2176
|
+
if (isBinary) {
|
|
2177
|
+
sim = hammingSimUI(queryEmbed, docEmbed);
|
|
2178
|
+
} else {
|
|
2179
|
+
sim = cosineSim(queryEmbed, docEmbed);
|
|
2180
|
+
}
|
|
2181
|
+
return { index: i, text, similarity: sim };
|
|
2182
|
+
}).sort((a, b) => b.similarity - a.similarity);
|
|
2183
|
+
|
|
2184
|
+
// Calculate storage
|
|
2185
|
+
const actualDims = isBinary ? dims * 8 : dims;
|
|
2186
|
+
let bytesPerVec;
|
|
2187
|
+
if (dtype === 'float') bytesPerVec = dims * 4;
|
|
2188
|
+
else if (dtype === 'int8' || dtype === 'uint8') bytesPerVec = dims * 1;
|
|
2189
|
+
else bytesPerVec = dims; // binary/ubinary: dims is already 1/8th
|
|
2190
|
+
|
|
2191
|
+
resultsByDtype[dtype] = {
|
|
2192
|
+
dtype, latency: elapsed, dims, actualDims, bytesPerVec,
|
|
2193
|
+
tokens: data.usage?.total_tokens || 0, ranked,
|
|
2194
|
+
};
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
const completed = Object.values(resultsByDtype);
|
|
2198
|
+
if (completed.length === 0) {
|
|
2199
|
+
showError('quantError', 'No data types completed successfully');
|
|
2200
|
+
return;
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
// ── Render Charts ──
|
|
2204
|
+
const baseline = completed.find(r => r.dtype === 'float') || completed[0];
|
|
2205
|
+
const maxBytes = Math.max(...completed.map(r => r.bytesPerVec));
|
|
2206
|
+
const maxLatency = Math.max(...completed.map(r => r.latency));
|
|
2207
|
+
const DTYPE_COLORS = { float: '#00d4aa', int8: '#4ecdc4', uint8: '#45b7d1', ubinary: '#ffd93d', binary: '#ff6b6b' };
|
|
2208
|
+
|
|
2209
|
+
// ── Storage Bar Chart ──
|
|
2210
|
+
let storageHTML = '';
|
|
2211
|
+
for (const r of completed) {
|
|
2212
|
+
const pct = Math.max(8, (r.bytesPerVec / maxBytes) * 100);
|
|
2213
|
+
const totalMB = (r.bytesPerVec * 1_000_000) / (1024 * 1024);
|
|
2214
|
+
const sizeStr = totalMB >= 1024 ? `${(totalMB / 1024).toFixed(1)} GB` : `${totalMB.toFixed(0)} MB`;
|
|
2215
|
+
const savings = r.bytesPerVec < baseline.bytesPerVec
|
|
2216
|
+
? `${(baseline.bytesPerVec / r.bytesPerVec).toFixed(0)}× smaller`
|
|
2217
|
+
: 'baseline';
|
|
2218
|
+
const color = DTYPE_COLORS[r.dtype] || '#82aaff';
|
|
2219
|
+
storageHTML += `<div class="quant-bar-group">
|
|
2220
|
+
<div class="quant-bar-label">
|
|
2221
|
+
<span class="dtype-name">${r.dtype}</span>
|
|
2222
|
+
<span class="dtype-value">${formatBytesUI(r.bytesPerVec)}/vec · ${sizeStr} @ 1M</span>
|
|
2223
|
+
</div>
|
|
2224
|
+
<div class="quant-bar-track">
|
|
2225
|
+
<div class="quant-bar-fill storage" style="width:${pct}%;background:linear-gradient(90deg, ${color}, ${color}cc);">${savings}</div>
|
|
2226
|
+
</div>
|
|
2227
|
+
</div>`;
|
|
2228
|
+
}
|
|
2229
|
+
document.getElementById('quantStorageChart').innerHTML = storageHTML;
|
|
2230
|
+
|
|
2231
|
+
// ── Latency Bar Chart ──
|
|
2232
|
+
let latencyHTML = '';
|
|
2233
|
+
const minLatency = Math.min(...completed.map(r => r.latency));
|
|
2234
|
+
for (const r of completed) {
|
|
2235
|
+
const pct = Math.max(8, (r.latency / maxLatency) * 100);
|
|
2236
|
+
const color = DTYPE_COLORS[r.dtype] || '#82aaff';
|
|
2237
|
+
const badge = r.latency === minLatency ? ' ⚡' : '';
|
|
2238
|
+
latencyHTML += `<div class="quant-bar-group">
|
|
2239
|
+
<div class="quant-bar-label">
|
|
2240
|
+
<span class="dtype-name">${r.dtype}</span>
|
|
2241
|
+
<span class="dtype-value">${r.latency.toFixed(0)}ms${badge}</span>
|
|
2242
|
+
</div>
|
|
2243
|
+
<div class="quant-bar-track">
|
|
2244
|
+
<div class="quant-bar-fill latency" style="width:${pct}%;background:linear-gradient(90deg, ${color}, ${color}cc);">${r.latency.toFixed(0)}ms</div>
|
|
2245
|
+
</div>
|
|
2246
|
+
</div>`;
|
|
2247
|
+
}
|
|
2248
|
+
document.getElementById('quantLatencyChart').innerHTML = latencyHTML;
|
|
2249
|
+
|
|
2250
|
+
// ── Quality Meters + Ranking Grid ──
|
|
2251
|
+
const topK = Math.min(5, corpus.length);
|
|
2252
|
+
const metersEl = document.getElementById('quantQualityMeters');
|
|
2253
|
+
const gridEl = document.getElementById('quantRankGrid');
|
|
2254
|
+
gridEl.innerHTML = '';
|
|
2255
|
+
metersEl.innerHTML = '';
|
|
2256
|
+
|
|
2257
|
+
if (completed.length >= 2 && baseline) {
|
|
2258
|
+
const baselineRanking = baseline.ranked.slice(0, topK).map(r => r.index);
|
|
2259
|
+
|
|
2260
|
+
// Quality meters for each non-baseline dtype
|
|
2261
|
+
let metersHTML = '';
|
|
2262
|
+
for (const r of completed) {
|
|
2263
|
+
if (r.dtype === baseline.dtype) continue;
|
|
2264
|
+
const otherRanking = r.ranked.slice(0, topK).map(x => x.index);
|
|
2265
|
+
const overlap = baselineRanking.filter(idx => otherRanking.includes(idx)).length;
|
|
2266
|
+
const overlapPct = (overlap / topK) * 100;
|
|
2267
|
+
const exactMatch = baselineRanking.every((idx, pos) => otherRanking[pos] === idx);
|
|
2268
|
+
const positionMatches = baselineRanking.filter((idx, pos) => otherRanking[pos] === idx).length;
|
|
2269
|
+
const posMatchPct = (positionMatches / topK) * 100;
|
|
2270
|
+
|
|
2271
|
+
let grade, gradeLabel, detail;
|
|
2272
|
+
if (exactMatch) {
|
|
2273
|
+
grade = 'perfect'; gradeLabel = '✓ Perfect';
|
|
2274
|
+
detail = `Identical ranking — all ${topK} positions match float baseline`;
|
|
2275
|
+
} else if (overlap === topK) {
|
|
2276
|
+
grade = 'good'; gradeLabel = '≈ Reordered';
|
|
2277
|
+
detail = `Same ${topK} documents, ${positionMatches}/${topK} in same position`;
|
|
2278
|
+
} else {
|
|
2279
|
+
grade = overlap >= topK * 0.6 ? 'good' : 'degraded';
|
|
2280
|
+
gradeLabel = `${overlapPct.toFixed(0)}% overlap`;
|
|
2281
|
+
detail = `${overlap}/${topK} documents match, ${positionMatches}/${topK} positions match`;
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
metersHTML += `<div class="quant-quality-meter">
|
|
2285
|
+
<div class="quant-meter-header">
|
|
2286
|
+
<span class="dtype-name">${r.dtype}</span>
|
|
2287
|
+
<span class="verdict-badge ${grade}">${gradeLabel}</span>
|
|
2288
|
+
</div>
|
|
2289
|
+
<div class="quant-meter-track">
|
|
2290
|
+
<div class="quant-meter-fill ${grade}" style="width:${exactMatch ? 100 : posMatchPct}%"></div>
|
|
2291
|
+
</div>
|
|
2292
|
+
<div class="quant-meter-detail">${detail}</div>
|
|
2293
|
+
</div>`;
|
|
2294
|
+
}
|
|
2295
|
+
metersEl.innerHTML = metersHTML;
|
|
2296
|
+
|
|
2297
|
+
// Side-by-side ranking columns
|
|
2298
|
+
let rankHTML = `<div class="quant-rank-cols" style="grid-template-columns:repeat(${completed.length},1fr);">`;
|
|
2299
|
+
for (const r of completed) {
|
|
2300
|
+
rankHTML += `<div><div class="quant-rank-col-header">${r.dtype}${r === baseline ? ' (baseline)' : ''}</div>`;
|
|
2301
|
+
r.ranked.slice(0, topK).forEach((item, pos) => {
|
|
2302
|
+
const trunc = item.text.length > 55 ? item.text.slice(0, 52) + '…' : item.text;
|
|
2303
|
+
let cls = 'baseline';
|
|
2304
|
+
if (r !== baseline) {
|
|
2305
|
+
cls = (baseline.ranked[pos] && item.index === baseline.ranked[pos].index) ? 'match' : 'differ';
|
|
2306
|
+
}
|
|
2307
|
+
rankHTML += `<div class="quant-rank-item ${cls}" title="${item.text.replace(/"/g, '"')}">
|
|
2308
|
+
<span class="quant-rank-pos">${pos + 1}</span>${trunc}
|
|
2309
|
+
<div class="quant-rank-score">${item.similarity.toFixed(4)} · doc ${item.index}</div>
|
|
2310
|
+
</div>`;
|
|
2311
|
+
});
|
|
2312
|
+
rankHTML += '</div>';
|
|
2313
|
+
}
|
|
2314
|
+
rankHTML += '</div>';
|
|
2315
|
+
gridEl.innerHTML = rankHTML;
|
|
2316
|
+
} else {
|
|
2317
|
+
metersEl.innerHTML = '<span style="color:var(--text-dim)">Select multiple data types (including float) to compare rankings.</span>';
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
document.getElementById('quantResult').classList.add('visible');
|
|
2321
|
+
} catch (err) {
|
|
2322
|
+
showError('quantError', err.message);
|
|
2323
|
+
} finally {
|
|
2324
|
+
setLoading('quantBtn', false);
|
|
2325
|
+
}
|
|
2326
|
+
};
|
|
2327
|
+
|
|
1800
2328
|
// ── Benchmark: Cost Calculator ──
|
|
1801
2329
|
function initCostCalculator() {
|
|
1802
2330
|
const tokSlider = document.getElementById('costTokens');
|
|
@@ -1936,6 +2464,7 @@ init = async function() {
|
|
|
1936
2464
|
await _origInit();
|
|
1937
2465
|
buildModelCheckboxes();
|
|
1938
2466
|
populateBenchRankSelects();
|
|
2467
|
+
populateQuantModelSelect();
|
|
1939
2468
|
initCostCalculator();
|
|
1940
2469
|
renderHistory();
|
|
1941
2470
|
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { describe, it } = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const { Command } = require('commander');
|
|
6
|
+
const { registerAbout } = require('../../src/commands/about');
|
|
7
|
+
|
|
8
|
+
describe('about command', () => {
|
|
9
|
+
it('registers correctly on a program', () => {
|
|
10
|
+
const program = new Command();
|
|
11
|
+
registerAbout(program);
|
|
12
|
+
const aboutCmd = program.commands.find(c => c.name() === 'about');
|
|
13
|
+
assert.ok(aboutCmd, 'about command should be registered');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('has --json option', () => {
|
|
17
|
+
const program = new Command();
|
|
18
|
+
registerAbout(program);
|
|
19
|
+
const aboutCmd = program.commands.find(c => c.name() === 'about');
|
|
20
|
+
const optionNames = aboutCmd.options.map(o => o.long);
|
|
21
|
+
assert.ok(optionNames.includes('--json'), 'should have --json option');
|
|
22
|
+
});
|
|
23
|
+
});
|