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>
Signed| @@ -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 | } |
| 1162 | 1171 | ||
| @@ -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 | } |
| 1196 | 1205 | ||
| 1206 | async 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 | |||
| 1225 | function 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 | |||
| 1197 | async function loadElectronHubModels() { | 1240 | async 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); |
| @@ -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 Model | 53 | Vectorization Model |
| @@ -493,6 +493,43 @@ router.post('/chutes/models/embedding', async (request, response) => { | |||
| 493 | } | 493 | } |
| 494 | }); | 494 | }); |
| 495 | 495 | ||
| 496 | router.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 | |||
| 496 | router.post('/generate-image', async (request, response) => { | 533 | router.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); |
| @@ -37,6 +37,7 @@ const SOURCES = [ | |||
| 37 | 'electronhub', | 37 | 'electronhub', |
| 38 | 'openrouter', | 38 | 'openrouter', |
| 39 | 'chutes', | 39 | 'chutes', |
| 40 | 'nanogpt', | ||
| 40 | ]; | 41 | ]; |
| 41 | 42 | ||
| 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 | } |
| 86 | 89 | ||
| 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 | } |
| @@ -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 | }; |
| 51 | 58 | ||
| 52 | /** | 59 | /** |