Add NanoGPT embeddings support for Vector Storage (#5150) * Initial plan * Add NanoGPT embeddings support for Vector Storage Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> * Fix models loading --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com>

5832cb8b07f8e7c62880b77be49b765dccb6fe49

Copilot <198982749+Copilot@users.noreply.github.com>

Signed
5 files changed, +112 -0Ignore whitespace
public/scripts/extensions/vectors/index.js+48 -0
@@ -71,6 +71,7 @@ const settings = {
71 webllm_model: '',71 webllm_model: '',
72 google_model: 'text-embedding-005',72 google_model: 'text-embedding-005',
73 chutes_model: 'chutes-qwen-qwen3-embedding-8b',73 chutes_model: 'chutes-qwen-qwen3-embedding-8b',
74 nanogpt_model: 'text-embedding-3-small',
74 summarize: false,75 summarize: false,
75 summarize_sent: false,76 summarize_sent: false,
76 summary_source: 'main',77 summary_source: 'main',
@@ -834,6 +835,9 @@ function getVectorsRequestBody(args = {}) {
834 case 'chutes':835 case 'chutes':
835 body.model = extension_settings.vectors.chutes_model;836 body.model = extension_settings.vectors.chutes_model;
836 break;837 break;
838 case 'nanogpt':
839 body.model = extension_settings.vectors.nanogpt_model;
840 break;
837 default:841 default:
838 break;842 break;
839 }843 }
@@ -919,6 +923,7 @@ function throwIfSourceInvalid() {
919 if (settings.source === 'openai' && !secret_state[SECRET_KEYS.OPENAI] ||923 if (settings.source === 'openai' && !secret_state[SECRET_KEYS.OPENAI] ||
920 settings.source === 'electronhub' && !secret_state[SECRET_KEYS.ELECTRONHUB] ||924 settings.source === 'electronhub' && !secret_state[SECRET_KEYS.ELECTRONHUB] ||
921 settings.source === 'chutes' && !secret_state[SECRET_KEYS.CHUTES] ||925 settings.source === 'chutes' && !secret_state[SECRET_KEYS.CHUTES] ||
926 settings.source === 'nanogpt' && !secret_state[SECRET_KEYS.NANOGPT] ||
922 settings.source === 'openrouter' && !secret_state[SECRET_KEYS.OPENROUTER] ||927 settings.source === 'openrouter' && !secret_state[SECRET_KEYS.OPENROUTER] ||
923 settings.source === 'palm' && !secret_state[SECRET_KEYS.MAKERSUITE] ||928 settings.source === 'palm' && !secret_state[SECRET_KEYS.MAKERSUITE] ||
924 settings.source === 'vertexai' && !secret_state[SECRET_KEYS.VERTEXAI] && !secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT] ||929 settings.source === 'vertexai' && !secret_state[SECRET_KEYS.VERTEXAI] && !secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT] ||
@@ -1134,6 +1139,7 @@ function toggleSettings() {
1134 $('#openai_vectorsModel').toggle(settings.source === 'openai');1139 $('#openai_vectorsModel').toggle(settings.source === 'openai');
1135 $('#electronhub_vectorsModel').toggle(settings.source === 'electronhub');1140 $('#electronhub_vectorsModel').toggle(settings.source === 'electronhub');
1136 $('#chutes_vectorsModel').toggle(settings.source === 'chutes');1141 $('#chutes_vectorsModel').toggle(settings.source === 'chutes');
1142 $('#nanogpt_vectorsModel').toggle(settings.source === 'nanogpt');
1137 $('#openrouter_vectorsModel').toggle(settings.source === 'openrouter');1143 $('#openrouter_vectorsModel').toggle(settings.source === 'openrouter');
1138 $('#cohere_vectorsModel').toggle(settings.source === 'cohere');1144 $('#cohere_vectorsModel').toggle(settings.source === 'cohere');
1139 $('#ollama_vectorsModel').toggle(settings.source === 'ollama');1145 $('#ollama_vectorsModel').toggle(settings.source === 'ollama');
@@ -1157,6 +1163,9 @@ function toggleSettings() {
1157 case 'chutes':1163 case 'chutes':
1158 loadChutesModels();1164 loadChutesModels();
1159 break;1165 break;
1166 case 'nanogpt':
1167 loadNanoGPTModels();
1168 break;
1160 }1169 }
1161}1170}
11621171
@@ -1194,6 +1203,40 @@ function populateChutesModelSelect(models) {
1194 $('#vectors_chutes_model').val(settings.chutes_model);1203 $('#vectors_chutes_model').val(settings.chutes_model);
1195}1204}
11961205
1206async function loadNanoGPTModels() {
1207 try {
1208 const response = await fetch('/api/openai/nanogpt/models/embedding', {
1209 method: 'POST',
1210 headers: getRequestHeaders({ omitContentType: true }),
1211 });
1212 if (!response.ok) {
1213 throw new Error(`HTTP ${response.status}`);
1214 }
1215 /** @type {Array<any>} */
1216 const data = await response.json();
1217 const models = Array.isArray(data) ? data : [];
1218 populateNanoGPTModelSelect(models);
1219 } catch (err) {
1220 console.warn('NanoGPT models fetch failed', err);
1221 populateNanoGPTModelSelect([]);
1222 }
1223}
1224
1225function populateNanoGPTModelSelect(models) {
1226 const select = $('#vectors_nanogpt_model');
1227 select.empty();
1228 for (const m of models) {
1229 const option = document.createElement('option');
1230 option.value = m.id;
1231 option.text = m.name || m.id;
1232 select.append(option);
1233 }
1234 if (!settings.nanogpt_model && models.length) {
1235 settings.nanogpt_model = models[0].id;
1236 }
1237 $('#vectors_nanogpt_model').val(settings.nanogpt_model);
1238}
1239
1197async function loadElectronHubModels() {1240async function loadElectronHubModels() {
1198 try {1241 try {
1199 const response = await fetch('/api/openai/electronhub/models', {1242 const response = await fetch('/api/openai/electronhub/models', {
@@ -1679,6 +1722,11 @@ jQuery(async () => {
1679 Object.assign(extension_settings.vectors, settings);1722 Object.assign(extension_settings.vectors, settings);
1680 saveSettingsDebounced();1723 saveSettingsDebounced();
1681 });1724 });
1725 $('#vectors_nanogpt_model').val(settings.nanogpt_model).on('change', () => {
1726 settings.nanogpt_model = String($('#vectors_nanogpt_model').val());
1727 Object.assign(extension_settings.vectors, settings);
1728 saveSettingsDebounced();
1729 });
1682 $('#vectors_openrouter_model').val(settings.openrouter_model).on('change', () => {1730 $('#vectors_openrouter_model').val(settings.openrouter_model).on('change', () => {
1683 settings.openrouter_model = String($('#vectors_openrouter_model').val());1731 settings.openrouter_model = String($('#vectors_openrouter_model').val());
1684 Object.assign(extension_settings.vectors, settings);1732 Object.assign(extension_settings.vectors, settings);
public/scripts/extensions/vectors/settings.html+10 -0
@@ -20,6 +20,7 @@
20 <option value="llamacpp">llama.cpp</option>20 <option value="llamacpp">llama.cpp</option>
21 <option value="transformers" data-i18n="Local (Transformers)">Local (Transformers)</option>21 <option value="transformers" data-i18n="Local (Transformers)">Local (Transformers)</option>
22 <option value="mistral">MistralAI</option>22 <option value="mistral">MistralAI</option>
23 <option value="nanogpt">NanoGPT</option>
23 <option value="nomicai">NomicAI</option>24 <option value="nomicai">NomicAI</option>
24 <option value="ollama">Ollama</option>25 <option value="ollama">Ollama</option>
25 <option value="openai">OpenAI</option>26 <option value="openai">OpenAI</option>
@@ -38,6 +39,15 @@
38 Hint: Set your Chutes API key in API Connections.39 Hint: Set your Chutes API key in API Connections.
39 </i>40 </i>
40 </div>41 </div>
42 <div class="flex-container flexFlowColumn" id="nanogpt_vectorsModel">
43 <label for="vectors_nanogpt_model" data-i18n="Vectorization Model">
44 Vectorization Model
45 </label>
46 <select id="vectors_nanogpt_model" class="text_pole"></select>
47 <i data-i18n="Hint: Set your NanoGPT API key in API Connections.">
48 Hint: Set your NanoGPT API key in API Connections.
49 </i>
50 </div>
41 <div class="flex-container flexFlowColumn" id="electronhub_vectorsModel">51 <div class="flex-container flexFlowColumn" id="electronhub_vectorsModel">
42 <label for="vectors_electronhub_model" data-i18n="Vectorization Model">52 <label for="vectors_electronhub_model" data-i18n="Vectorization Model">
43 Vectorization Model53 Vectorization Model
src/endpoints/openai.js+37 -0
@@ -493,6 +493,43 @@ router.post('/chutes/models/embedding', async (request, response) => {
493 }493 }
494});494});
495495
496router.post('/nanogpt/models/embedding', async (request, response) => {
497 try {
498 const key = readSecret(request.user.directories, SECRET_KEYS.NANOGPT);
499
500 if (!key) {
501 console.warn('No NanoGPT key found');
502 return response.sendStatus(400);
503 }
504
505 const result = await fetch('https://nano-gpt.com/api/v1/embedding-models', {
506 method: 'GET',
507 headers: {
508 'Authorization': `Bearer ${key}`,
509 'Accept-Encoding': 'identity',
510 },
511 });
512
513 if (!result.ok) {
514 const text = await result.text();
515 console.warn('NanoGPT embedding models request failed', result.statusText, text);
516 return response.status(500).send(text);
517 }
518
519 /** @type {any} */
520 const data = await result.json();
521
522 if (!Array.isArray(data?.data)) {
523 console.warn('NanoGPT embedding models response invalid', data);
524 return response.sendStatus(500);
525 }
526 return response.json(data.data);
527 } catch (error) {
528 console.error('NanoGPT embedding models fetch failed', error);
529 response.sendStatus(500);
530 }
531});
532
496router.post('/generate-image', async (request, response) => {533router.post('/generate-image', async (request, response) => {
497 try {534 try {
498 const key = readSecret(request.user.directories, SECRET_KEYS.OPENAI);535 const key = readSecret(request.user.directories, SECRET_KEYS.OPENAI);
src/endpoints/vectors.js+10 -0
@@ -37,6 +37,7 @@ const SOURCES = [
37 'electronhub',37 'electronhub',
38 'openrouter',38 'openrouter',
39 'chutes',39 'chutes',
40 'nanogpt',
40];41];
4142
42/**43/**
@@ -82,6 +83,8 @@ async function getVector(source, sourceSettings, text, isQuery, directories) {
82 return sourceSettings.embeddings[text];83 return sourceSettings.embeddings[text];
83 case 'chutes':84 case 'chutes':
84 return getOpenAIVector(text, source, directories, sourceSettings.model);85 return getOpenAIVector(text, source, directories, sourceSettings.model);
86 case 'nanogpt':
87 return getOpenAIVector(text, source, directories, sourceSettings.model);
85 }88 }
8689
87 throw new Error(`Unknown vector source ${source}`);90 throw new Error(`Unknown vector source ${source}`);
@@ -150,6 +153,9 @@ async function getBatchVector(source, sourceSettings, texts, isQuery, directorie
150 case 'chutes':153 case 'chutes':
151 results.push(...await getOpenAIBatchVector(batch, source, directories, sourceSettings.model));154 results.push(...await getOpenAIBatchVector(batch, source, directories, sourceSettings.model));
152 break;155 break;
156 case 'nanogpt':
157 results.push(...await getOpenAIBatchVector(batch, source, directories, sourceSettings.model));
158 break;
153 default:159 default:
154 throw new Error(`Unknown vector source ${source}`);160 throw new Error(`Unknown vector source ${source}`);
155 }161 }
@@ -238,6 +244,10 @@ function getSourceSettings(source, request) {
238 return {244 return {
239 model: String(request.body.model || 'chutes-qwen-qwen3-embedding-8b'),245 model: String(request.body.model || 'chutes-qwen-qwen3-embedding-8b'),
240 };246 };
247 case 'nanogpt':
248 return {
249 model: String(request.body.model || 'text-embedding-3-small'),
250 };
241 default:251 default:
242 return {};252 return {};
243 }253 }
src/vectors/openai-vectors.js+7 -0
@@ -47,6 +47,13 @@ const SOURCES = {
47 body.model = null;47 body.model = null;
48 },48 },
49 },49 },
50 'nanogpt': {
51 secretKey: SECRET_KEYS.NANOGPT,
52 url: 'https://nano-gpt.com/api/v1',
53 model: 'text-embedding-3-small',
54 headers: {},
55 processBody: () => {},
56 },
50};57};
5158
52/**59/**