voyageai-cli 1.26.0 → 1.27.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.
@@ -40,180 +40,198 @@ function suggestTopics(input, topics) {
40
40
  }
41
41
 
42
42
  /**
43
- * Register utility tools: vai_topics, vai_explain, vai_estimate
44
- * @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} server
45
- * @param {object} schemas
43
+ * Handler for vai_topics: list all available educational topics.
44
+ * @param {object} input - Validated input matching topicsSchema
45
+ * @returns {Promise<{structuredContent: object, content: Array}>}
46
46
  */
47
- function registerUtilityTools(server, schemas) {
48
- // vai_topics list all available educational topics
49
- server.tool(
50
- 'vai_topics',
51
- 'List all available educational topics with summaries. Call this FIRST to discover what topics vai can explain — covers embeddings, vector search, RAG, reranking, model selection, multimodal, code generation, and more. Then use vai_explain to dive deep into any topic.',
52
- schemas.topicsSchema,
53
- async (input) => {
54
- const allTopics = listConcepts();
55
-
56
- let topics;
57
- if (input.search) {
58
- // Filter topics by search term
59
- const suggestions = suggestTopics(input.search, allTopics);
60
- if (suggestions.length === 0) {
61
- return {
62
- structuredContent: { search: input.search, results: [], totalTopics: allTopics.length },
63
- content: [{ type: 'text', text: `No topics matching "${input.search}". Use vai_topics without a search to see all ${allTopics.length} topics.` }],
64
- };
65
- }
66
- topics = suggestions;
67
- } else {
68
- // List all topics with summaries
69
- topics = allTopics.map(key => {
70
- const concept = getConcept(key);
71
- return { topic: key, title: concept.title, summary: concept.summary };
72
- });
73
- }
74
-
75
- // Group by category for better browsing
76
- const categories = {
77
- 'Core Concepts': ['embeddings', 'vector-search', 'rag', 'cosine-similarity', 'input-type', 'two-stage-retrieval'],
78
- 'Models & Pricing': ['models', 'mixture-of-experts', 'voyage-4-nano', 'shared-embedding-space', 'quantization', 'benchmarking', 'rteb-benchmarks', 'provider-comparison'],
79
- 'Multimodal': ['multimodal-embeddings', 'cross-modal-search', 'modality-gap', 'multimodal-rag'],
80
- 'API & Configuration': ['api-keys', 'api-access', 'batch-processing', 'auto-embedding', 'vai-vs-auto-embedding'],
81
- 'Reranking & Evaluation': ['reranking', 'rerank-eval', 'eval-comparison'],
82
- 'Code & Chat': ['code-generation', 'scaffolding', 'chat'],
83
- };
84
-
85
- const textLines = topics.map(t => `• **${t.topic}** — ${t.summary}`);
86
- const searchNote = input.search ? ` matching "${input.search}"` : '';
87
-
47
+ async function handleVaiTopics(input) {
48
+ const allTopics = listConcepts();
49
+
50
+ let topics;
51
+ if (input.search) {
52
+ // Filter topics by search term
53
+ const suggestions = suggestTopics(input.search, allTopics);
54
+ if (suggestions.length === 0) {
88
55
  return {
89
- structuredContent: {
90
- search: input.search || null,
91
- topics,
92
- categories: input.search ? undefined : categories,
93
- totalTopics: allTopics.length,
94
- },
95
- content: [{
96
- type: 'text',
97
- text: `${topics.length} topic${topics.length === 1 ? '' : 's'}${searchNote} available:\n\n${textLines.join('\n')}\n\nUse vai_explain with any topic name to get the full explanation.`,
98
- }],
56
+ structuredContent: { search: input.search, results: [], totalTopics: allTopics.length },
57
+ content: [{ type: 'text', text: `No topics matching "${input.search}". Use vai_topics without a search to see all ${allTopics.length} topics.` }],
99
58
  };
100
59
  }
101
- );
102
-
103
- // vai_explain educational content with fuzzy matching
104
- server.tool(
105
- 'vai_explain',
106
- 'Get a detailed explanation of a topic. Covers embeddings, vector search, RAG, MoE architecture, shared space, quantization, multimodal, reranking, and more. If the exact topic isn\'t found, suggests similar topics. Use vai_topics first to browse available topics.',
107
- schemas.explainSchema,
108
- async (input) => {
109
- const key = resolveConcept(input.topic);
110
- if (!key) {
111
- // Try fuzzy matching before giving up
112
- const allTopics = listConcepts();
113
- const suggestions = suggestTopics(input.topic, allTopics);
114
-
115
- if (suggestions.length > 0) {
116
- // Auto-resolve if there's a strong match
117
- const bestMatch = suggestions[0];
118
- const bestKey = resolveConcept(bestMatch.topic);
119
- if (bestKey) {
120
- const concept = getConcept(bestKey);
121
- return {
122
- structuredContent: {
123
- topic: bestKey,
124
- title: concept.title,
125
- summary: concept.summary,
126
- content: concept.content,
127
- links: concept.links || [],
128
- matchedFrom: input.topic,
129
- relatedTopics: suggestions.slice(1).map(s => s.topic),
130
- },
131
- content: [{
132
- type: 'text',
133
- text: `# ${concept.title}\n\n${concept.summary}\n\n${concept.content}${suggestions.length > 1 ? `\n\n---\nRelated topics: ${suggestions.slice(1).map(s => s.topic).join(', ')}` : ''}`,
134
- }],
135
- };
136
- }
137
- }
60
+ topics = suggestions;
61
+ } else {
62
+ // List all topics with summaries
63
+ topics = allTopics.map(key => {
64
+ const concept = getConcept(key);
65
+ return { topic: key, title: concept.title, summary: concept.summary };
66
+ });
67
+ }
68
+
69
+ // Group by category for better browsing
70
+ const categories = {
71
+ 'Core Concepts': ['embeddings', 'vector-search', 'rag', 'cosine-similarity', 'input-type', 'two-stage-retrieval'],
72
+ 'Models & Pricing': ['models', 'mixture-of-experts', 'voyage-4-nano', 'shared-embedding-space', 'quantization', 'benchmarking', 'rteb-benchmarks', 'provider-comparison'],
73
+ 'Multimodal': ['multimodal-embeddings', 'cross-modal-search', 'modality-gap', 'multimodal-rag'],
74
+ 'API & Configuration': ['api-keys', 'api-access', 'batch-processing', 'auto-embedding', 'vai-vs-auto-embedding'],
75
+ 'Reranking & Evaluation': ['reranking', 'rerank-eval', 'eval-comparison'],
76
+ 'Code & Chat': ['code-generation', 'scaffolding', 'chat'],
77
+ };
78
+
79
+ const textLines = topics.map(t => `• **${t.topic}** — ${t.summary}`);
80
+ const searchNote = input.search ? ` matching "${input.search}"` : '';
81
+
82
+ return {
83
+ structuredContent: {
84
+ search: input.search || null,
85
+ topics,
86
+ categories: input.search ? undefined : categories,
87
+ totalTopics: allTopics.length,
88
+ },
89
+ content: [{
90
+ type: 'text',
91
+ text: `${topics.length} topic${topics.length === 1 ? '' : 's'}${searchNote} available:\n\n${textLines.join('\n')}\n\nUse vai_explain with any topic name to get the full explanation.`,
92
+ }],
93
+ };
94
+ }
138
95
 
96
+ /**
97
+ * Handler for vai_explain: educational content with fuzzy matching.
98
+ * @param {object} input - Validated input matching explainSchema
99
+ * @returns {Promise<{structuredContent: object, content: Array}>}
100
+ */
101
+ async function handleVaiExplain(input) {
102
+ const key = resolveConcept(input.topic);
103
+ if (!key) {
104
+ // Try fuzzy matching before giving up
105
+ const allTopics = listConcepts();
106
+ const suggestions = suggestTopics(input.topic, allTopics);
107
+
108
+ if (suggestions.length > 0) {
109
+ // Auto-resolve if there's a strong match
110
+ const bestMatch = suggestions[0];
111
+ const bestKey = resolveConcept(bestMatch.topic);
112
+ if (bestKey) {
113
+ const concept = getConcept(bestKey);
139
114
  return {
140
- structuredContent: { error: 'unknown_topic', topic: input.topic, suggestions, available: allTopics },
115
+ structuredContent: {
116
+ topic: bestKey,
117
+ title: concept.title,
118
+ summary: concept.summary,
119
+ content: concept.content,
120
+ links: concept.links || [],
121
+ matchedFrom: input.topic,
122
+ relatedTopics: suggestions.slice(1).map(s => s.topic),
123
+ },
141
124
  content: [{
142
125
  type: 'text',
143
- text: suggestions.length > 0
144
- ? `No exact match for "${input.topic}". Did you mean:\n\n${suggestions.map(s => `• **${s.topic}** — ${s.summary}`).join('\n')}\n\nUse vai_topics to see all ${allTopics.length} available topics.`
145
- : `Unknown topic: "${input.topic}"\n\nUse vai_topics to browse all ${allTopics.length} available topics.`,
126
+ text: `# ${concept.title}\n\n${concept.summary}\n\n${concept.content}${suggestions.length > 1 ? `\n\n---\nRelated topics: ${suggestions.slice(1).map(s => s.topic).join(', ')}` : ''}`,
146
127
  }],
147
128
  };
148
129
  }
130
+ }
149
131
 
150
- const concept = getConcept(key);
151
-
152
- // Find related topics based on the current topic
153
- const allTopics = listConcepts().filter(t => t !== key);
154
- const related = suggestTopics(key, allTopics).slice(0, 3);
132
+ return {
133
+ structuredContent: { error: 'unknown_topic', topic: input.topic, suggestions, available: allTopics },
134
+ content: [{
135
+ type: 'text',
136
+ text: suggestions.length > 0
137
+ ? `No exact match for "${input.topic}". Did you mean:\n\n${suggestions.map(s => `• **${s.topic}** — ${s.summary}`).join('\n')}\n\nUse vai_topics to see all ${allTopics.length} available topics.`
138
+ : `Unknown topic: "${input.topic}"\n\nUse vai_topics to browse all ${allTopics.length} available topics.`,
139
+ }],
140
+ };
141
+ }
142
+
143
+ const concept = getConcept(key);
144
+
145
+ // Find related topics based on the current topic
146
+ const allTopics = listConcepts().filter(t => t !== key);
147
+ const related = suggestTopics(key, allTopics).slice(0, 3);
148
+
149
+ return {
150
+ structuredContent: {
151
+ topic: key,
152
+ title: concept.title,
153
+ summary: concept.summary,
154
+ content: concept.content,
155
+ links: concept.links || [],
156
+ tryIt: concept.tryIt || null,
157
+ relatedTopics: related.map(r => r.topic),
158
+ },
159
+ content: [{
160
+ type: 'text',
161
+ text: `# ${concept.title}\n\n${concept.summary}\n\n${concept.content}${concept.links?.length ? `\n\n**Learn more:** ${concept.links.join(', ')}` : ''}${related.length ? `\n\n**Related:** ${related.map(r => r.topic).join(', ')}` : ''}`,
162
+ }],
163
+ };
164
+ }
155
165
 
166
+ /**
167
+ * Handler for vai_estimate: cost estimation.
168
+ * @param {object} input - Validated input matching estimateSchema
169
+ * @returns {Promise<{structuredContent: object, content: Array}>}
170
+ */
171
+ async function handleVaiEstimate(input) {
172
+ const { docs, queries, months } = input;
173
+ // Average tokens per doc chunk (~250 tokens)
174
+ const avgTokensPerDoc = 250;
175
+ const totalEmbedTokens = docs * avgTokensPerDoc;
176
+
177
+ const embeddingModels = MODEL_CATALOG
178
+ .filter(m => m.type === 'embedding' && !m.legacy && !m.unreleased && m.pricePerMToken)
179
+ .map(m => {
180
+ const embedCost = (totalEmbedTokens / 1_000_000) * m.pricePerMToken;
181
+ const queryCostPerMonth = queries > 0 ? (queries * avgTokensPerDoc / 1_000_000) * m.pricePerMToken : 0;
182
+ const totalCost = embedCost + (queryCostPerMonth * months);
156
183
  return {
157
- structuredContent: {
158
- topic: key,
159
- title: concept.title,
160
- summary: concept.summary,
161
- content: concept.content,
162
- links: concept.links || [],
163
- tryIt: concept.tryIt || null,
164
- relatedTopics: related.map(r => r.topic),
165
- },
166
- content: [{
167
- type: 'text',
168
- text: `# ${concept.title}\n\n${concept.summary}\n\n${concept.content}${concept.links?.length ? `\n\n**Learn more:** ${concept.links.join(', ')}` : ''}${related.length ? `\n\n**Related:** ${related.map(r => r.topic).join(', ')}` : ''}`,
169
- }],
184
+ model: m.name,
185
+ pricePerMToken: m.pricePerMToken,
186
+ embeddingCost: Math.round(embedCost * 100) / 100,
187
+ monthlyCost: Math.round(queryCostPerMonth * 100) / 100,
188
+ totalCost: Math.round(totalCost * 100) / 100,
170
189
  };
171
- }
190
+ })
191
+ .sort((a, b) => a.totalCost - b.totalCost);
192
+
193
+ const structured = {
194
+ input: { docs, queries, months },
195
+ estimates: embeddingModels,
196
+ recommendation: embeddingModels[0]?.model || getDefaultModel(),
197
+ };
198
+
199
+ const lines = embeddingModels.map(e =>
200
+ `• ${e.model}: embed $${e.embeddingCost} + $${e.monthlyCost}/mo queries = $${e.totalCost} total (${months}mo)`
201
+ );
202
+
203
+ return {
204
+ structuredContent: structured,
205
+ content: [{ type: 'text', text: `Cost estimate for ${docs.toLocaleString()} docs, ${queries.toLocaleString()} queries/mo over ${months} months:\n\n${lines.join('\n')}\n\nRecommended: ${structured.recommendation}` }],
206
+ };
207
+ }
208
+
209
+ /**
210
+ * Register utility tools: vai_topics, vai_explain, vai_estimate
211
+ * @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} server
212
+ * @param {object} schemas
213
+ */
214
+ function registerUtilityTools(server, schemas) {
215
+ server.tool(
216
+ 'vai_topics',
217
+ 'List all available educational topics with summaries. Call this FIRST to discover what topics vai can explain — covers embeddings, vector search, RAG, reranking, model selection, multimodal, code generation, and more. Then use vai_explain to dive deep into any topic.',
218
+ schemas.topicsSchema,
219
+ handleVaiTopics
220
+ );
221
+
222
+ server.tool(
223
+ 'vai_explain',
224
+ 'Get a detailed explanation of a topic. Covers embeddings, vector search, RAG, MoE architecture, shared space, quantization, multimodal, reranking, and more. If the exact topic isn\'t found, suggests similar topics. Use vai_topics first to browse available topics.',
225
+ schemas.explainSchema,
226
+ handleVaiExplain
172
227
  );
173
228
 
174
- // vai_estimate — cost estimation
175
229
  server.tool(
176
230
  'vai_estimate',
177
231
  'Estimate costs for Voyage AI embedding and query operations at various scales. Use when planning ingestion, budgeting, or comparing model costs.',
178
232
  schemas.estimateSchema,
179
- async (input) => {
180
- const { docs, queries, months } = input;
181
- // Average tokens per doc chunk (~250 tokens)
182
- const avgTokensPerDoc = 250;
183
- const totalEmbedTokens = docs * avgTokensPerDoc;
184
-
185
- const embeddingModels = MODEL_CATALOG
186
- .filter(m => m.type === 'embedding' && !m.legacy && !m.unreleased && m.pricePerMToken)
187
- .map(m => {
188
- const embedCost = (totalEmbedTokens / 1_000_000) * m.pricePerMToken;
189
- const queryCostPerMonth = queries > 0 ? (queries * avgTokensPerDoc / 1_000_000) * m.pricePerMToken : 0;
190
- const totalCost = embedCost + (queryCostPerMonth * months);
191
- return {
192
- model: m.name,
193
- pricePerMToken: m.pricePerMToken,
194
- embeddingCost: Math.round(embedCost * 100) / 100,
195
- monthlyCost: Math.round(queryCostPerMonth * 100) / 100,
196
- totalCost: Math.round(totalCost * 100) / 100,
197
- };
198
- })
199
- .sort((a, b) => a.totalCost - b.totalCost);
200
-
201
- const structured = {
202
- input: { docs, queries, months },
203
- estimates: embeddingModels,
204
- recommendation: embeddingModels[0]?.model || getDefaultModel(),
205
- };
206
-
207
- const lines = embeddingModels.map(e =>
208
- `• ${e.model}: embed $${e.embeddingCost} + $${e.monthlyCost}/mo queries = $${e.totalCost} total (${months}mo)`
209
- );
210
-
211
- return {
212
- structuredContent: structured,
213
- content: [{ type: 'text', text: `Cost estimate for ${docs.toLocaleString()} docs, ${queries.toLocaleString()} queries/mo over ${months} months:\n\n${lines.join('\n')}\n\nRecommended: ${structured.recommendation}` }],
214
- };
215
- }
233
+ handleVaiEstimate
216
234
  );
217
235
  }
218
236
 
219
- module.exports = { registerUtilityTools };
237
+ module.exports = { registerUtilityTools, handleVaiTopics, handleVaiExplain, handleVaiEstimate };
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file