OpenRouter: Add as vector storage provider (#4736) * OpenRouter: Add as vector storage provider * Better array filtering
Signed| @@ -62,6 +62,7 @@ const settings = { | |||
| 62 | togetherai_model: 'togethercomputer/m2-bert-80M-32k-retrieval', | 62 | togetherai_model: 'togethercomputer/m2-bert-80M-32k-retrieval', |
| 63 | openai_model: 'text-embedding-ada-002', | 63 | openai_model: 'text-embedding-ada-002', |
| 64 | electronhub_model: 'text-embedding-3-small', | 64 | electronhub_model: 'text-embedding-3-small', |
| 65 | openrouter_model: 'openai/text-embedding-3-large', | ||
| 65 | cohere_model: 'embed-english-v3.0', | 66 | cohere_model: 'embed-english-v3.0', |
| 66 | ollama_model: 'mxbai-embed-large', | 67 | ollama_model: 'mxbai-embed-large', |
| 67 | ollama_keep: false, | 68 | ollama_keep: false, |
| @@ -790,6 +791,9 @@ function getVectorsRequestBody(args = {}) { | |||
| 790 | case 'electronhub': | 791 | case 'electronhub': |
| 791 | body.model = extension_settings.vectors.electronhub_model; | 792 | body.model = extension_settings.vectors.electronhub_model; |
| 792 | break; | 793 | break; |
| 794 | case 'openrouter': | ||
| 795 | body.model = extension_settings.vectors.openrouter_model; | ||
| 796 | break; | ||
| 793 | case 'togetherai': | 797 | case 'togetherai': |
| 794 | body.model = extension_settings.vectors.togetherai_model; | 798 | body.model = extension_settings.vectors.togetherai_model; |
| 795 | break; | 799 | break; |
| @@ -909,6 +913,7 @@ async function insertVectorItems(collectionId, items) { | |||
| 909 | function throwIfSourceInvalid() { | 913 | function throwIfSourceInvalid() { |
| 910 | if (settings.source === 'openai' && !secret_state[SECRET_KEYS.OPENAI] || | 914 | if (settings.source === 'openai' && !secret_state[SECRET_KEYS.OPENAI] || |
| 911 | settings.source === 'electronhub' && !secret_state[SECRET_KEYS.ELECTRONHUB] || | 915 | settings.source === 'electronhub' && !secret_state[SECRET_KEYS.ELECTRONHUB] || |
| 916 | settings.source === 'openrouter' && !secret_state[SECRET_KEYS.OPENROUTER] || | ||
| 912 | settings.source === 'palm' && !secret_state[SECRET_KEYS.MAKERSUITE] || | 917 | settings.source === 'palm' && !secret_state[SECRET_KEYS.MAKERSUITE] || |
| 913 | settings.source === 'vertexai' && !secret_state[SECRET_KEYS.VERTEXAI] && !secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT] || | 918 | settings.source === 'vertexai' && !secret_state[SECRET_KEYS.VERTEXAI] && !secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT] || |
| 914 | settings.source === 'mistral' && !secret_state[SECRET_KEYS.MISTRALAI] || | 919 | settings.source === 'mistral' && !secret_state[SECRET_KEYS.MISTRALAI] || |
| @@ -1122,6 +1127,7 @@ function toggleSettings() { | |||
| 1122 | $('#together_vectorsModel').toggle(settings.source === 'togetherai'); | 1127 | $('#together_vectorsModel').toggle(settings.source === 'togetherai'); |
| 1123 | $('#openai_vectorsModel').toggle(settings.source === 'openai'); | 1128 | $('#openai_vectorsModel').toggle(settings.source === 'openai'); |
| 1124 | $('#electronhub_vectorsModel').toggle(settings.source === 'electronhub'); | 1129 | $('#electronhub_vectorsModel').toggle(settings.source === 'electronhub'); |
| 1130 | $('#openrouter_vectorsModel').toggle(settings.source === 'openrouter'); | ||
| 1125 | $('#cohere_vectorsModel').toggle(settings.source === 'cohere'); | 1131 | $('#cohere_vectorsModel').toggle(settings.source === 'cohere'); |
| 1126 | $('#ollama_vectorsModel').toggle(settings.source === 'ollama'); | 1132 | $('#ollama_vectorsModel').toggle(settings.source === 'ollama'); |
| 1127 | $('#llamacpp_vectorsModel').toggle(settings.source === 'llamacpp'); | 1133 | $('#llamacpp_vectorsModel').toggle(settings.source === 'llamacpp'); |
| @@ -1138,6 +1144,9 @@ function toggleSettings() { | |||
| 1138 | case 'electronhub': | 1144 | case 'electronhub': |
| 1139 | loadElectronHubModels(); | 1145 | loadElectronHubModels(); |
| 1140 | break; | 1146 | break; |
| 1147 | case 'openrouter': | ||
| 1148 | loadOpenRouterModels(); | ||
| 1149 | break; | ||
| 1141 | } | 1150 | } |
| 1142 | } | 1151 | } |
| 1143 | 1152 | ||
| @@ -1180,6 +1189,44 @@ function populateElectronHubModelSelect(models) { | |||
| 1180 | $('#vectors_electronhub_model').val(settings.electronhub_model); | 1189 | $('#vectors_electronhub_model').val(settings.electronhub_model); |
| 1181 | } | 1190 | } |
| 1182 | 1191 | ||
| 1192 | async function loadOpenRouterModels() { | ||
| 1193 | try { | ||
| 1194 | const response = await fetch('/api/openrouter/models/embedding', { | ||
| 1195 | method: 'POST', | ||
| 1196 | headers: getRequestHeaders(), | ||
| 1197 | }); | ||
| 1198 | if (!response.ok) { | ||
| 1199 | throw new Error(`HTTP ${response.status}`); | ||
| 1200 | } | ||
| 1201 | /** @type {Array<any>} */ | ||
| 1202 | const data = await response.json(); | ||
| 1203 | const models = Array.isArray(data) ? data : []; | ||
| 1204 | populateOpenRouterModelSelect(models); | ||
| 1205 | } catch (err) { | ||
| 1206 | console.warn('OpenRouter models fetch failed', err); | ||
| 1207 | populateOpenRouterModelSelect([]); | ||
| 1208 | } | ||
| 1209 | } | ||
| 1210 | |||
| 1211 | /** | ||
| 1212 | * Populates the OpenRouter model select element. | ||
| 1213 | * @param {{ id: string, name: string }[]} models OpenRouter models | ||
| 1214 | */ | ||
| 1215 | function populateOpenRouterModelSelect(models) { | ||
| 1216 | const select = $('#vectors_openrouter_model'); | ||
| 1217 | select.empty(); | ||
| 1218 | for (const m of models) { | ||
| 1219 | const option = document.createElement('option'); | ||
| 1220 | option.value = m.id; | ||
| 1221 | option.text = m.name || m.id; | ||
| 1222 | select.append(option); | ||
| 1223 | } | ||
| 1224 | if (!settings.openrouter_model && models.length) { | ||
| 1225 | settings.openrouter_model = models[0].id; | ||
| 1226 | } | ||
| 1227 | $('#vectors_openrouter_model').val(settings.openrouter_model); | ||
| 1228 | } | ||
| 1229 | |||
| 1183 | /** | 1230 | /** |
| 1184 | * Executes a function with WebLLM error handling. | 1231 | * Executes a function with WebLLM error handling. |
| 1185 | * @param {function(): Promise<T>} func Function to execute | 1232 | * @param {function(): Promise<T>} func Function to execute |
| @@ -1583,6 +1630,11 @@ jQuery(async () => { | |||
| 1583 | Object.assign(extension_settings.vectors, settings); | 1630 | Object.assign(extension_settings.vectors, settings); |
| 1584 | saveSettingsDebounced(); | 1631 | saveSettingsDebounced(); |
| 1585 | }); | 1632 | }); |
| 1633 | $('#vectors_openrouter_model').val(settings.openrouter_model).on('change', () => { | ||
| 1634 | settings.openrouter_model = String($('#vectors_openrouter_model').val()); | ||
| 1635 | Object.assign(extension_settings.vectors, settings); | ||
| 1636 | saveSettingsDebounced(); | ||
| 1637 | }); | ||
| 1586 | $('#vectors_cohere_model').val(settings.cohere_model).on('change', () => { | 1638 | $('#vectors_cohere_model').val(settings.cohere_model).on('change', () => { |
| 1587 | settings.cohere_model = String($('#vectors_cohere_model').val()); | 1639 | settings.cohere_model = String($('#vectors_cohere_model').val()); |
| 1588 | Object.assign(extension_settings.vectors, settings); | 1640 | Object.assign(extension_settings.vectors, settings); |
| @@ -22,6 +22,7 @@ | |||
| 22 | <option value="nomicai">NomicAI</option> | 22 | <option value="nomicai">NomicAI</option> |
| 23 | <option value="ollama">Ollama</option> | 23 | <option value="ollama">Ollama</option> |
| 24 | <option value="openai">OpenAI</option> | 24 | <option value="openai">OpenAI</option> |
| 25 | <option value="openrouter">OpenRouter</option> | ||
| 25 | <option value="togetherai">TogetherAI</option> | 26 | <option value="togetherai">TogetherAI</option> |
| 26 | <option value="vllm">vLLM</option> | 27 | <option value="vllm">vLLM</option> |
| 27 | <option value="webllm" data-i18n="WebLLM Extension">WebLLM Extension</option> | 28 | <option value="webllm" data-i18n="WebLLM Extension">WebLLM Extension</option> |
| @@ -163,6 +164,15 @@ | |||
| 163 | <span data-i18n="Click to set">Click to set</span> | 164 | <span data-i18n="Click to set">Click to set</span> |
| 164 | </div> | 165 | </div> |
| 165 | </div> | 166 | </div> |
| 167 | <div class="flex-container flexFlowColumn" id="openrouter_vectorsModel"> | ||
| 168 | <label for="vectors_openrouter_model" data-i18n="Vectorization Model"> | ||
| 169 | Vectorization Model | ||
| 170 | </label> | ||
| 171 | <select id="vectors_openrouter_model" class="text_pole"></select> | ||
| 172 | <i data-i18n="Hint: Set your OpenRouter API key in API Connections."> | ||
| 173 | Hint: Set your OpenRouter API key in API Connections. | ||
| 174 | </i> | ||
| 175 | </div> | ||
| 166 | 176 | ||
| 167 | <div class="flex-container marginTopBot5"> | 177 | <div class="flex-container marginTopBot5"> |
| 168 | <div class="flex-container flex1 flexFlowColumn" title="How many last messages will be matched for relevance."> | 178 | <div class="flex-container flex1 flexFlowColumn" title="How many last messages will be matched for relevance."> |
| @@ -55,3 +55,37 @@ router.post('/models/multimodal', async (_req, res) => { | |||
| 55 | return res.sendStatus(500); | 55 | return res.sendStatus(500); |
| 56 | } | 56 | } |
| 57 | }); | 57 | }); |
| 58 | |||
| 59 | router.post('/models/embedding', async (_req, res) => { | ||
| 60 | try { | ||
| 61 | // The endpoint is available without authentication | ||
| 62 | const response = await fetch(`${API_OPENROUTER}/models`, { | ||
| 63 | method: 'GET', | ||
| 64 | headers: { | ||
| 65 | 'Accept': 'application/json', | ||
| 66 | }, | ||
| 67 | }); | ||
| 68 | |||
| 69 | if (!response.ok) { | ||
| 70 | console.warn('OpenRouter API request failed', response.statusText); | ||
| 71 | return res.json([]); | ||
| 72 | } | ||
| 73 | |||
| 74 | /** @type {any} */ | ||
| 75 | const data = await response.json(); | ||
| 76 | |||
| 77 | if (!Array.isArray(data?.data)) { | ||
| 78 | console.warn('OpenRouter API response was not an array'); | ||
| 79 | return res.json([]); | ||
| 80 | } | ||
| 81 | |||
| 82 | const embeddingModels = data.data | ||
| 83 | .filter(m => Array.isArray(m?.architecture?.output_modalities)) | ||
| 84 | .filter(m => m.architecture.output_modalities.includes('embeddings')); | ||
| 85 | |||
| 86 | return res.json(embeddingModels); | ||
| 87 | } catch (error) { | ||
| 88 | console.error(error); | ||
| 89 | return res.sendStatus(500); | ||
| 90 | } | ||
| 91 | }); | ||
| @@ -35,6 +35,7 @@ const SOURCES = [ | |||
| 35 | 'koboldcpp', | 35 | 'koboldcpp', |
| 36 | 'vertexai', | 36 | 'vertexai', |
| 37 | 'electronhub', | 37 | 'electronhub', |
| 38 | 'openrouter', | ||
| 38 | ]; | 39 | ]; |
| 39 | 40 | ||
| 40 | /** | 41 | /** |
| @@ -56,6 +57,8 @@ async function getVector(source, sourceSettings, text, isQuery, directories) { | |||
| 56 | return getOpenAIVector(text, source, directories, sourceSettings.model); | 57 | return getOpenAIVector(text, source, directories, sourceSettings.model); |
| 57 | case 'electronhub': | 58 | case 'electronhub': |
| 58 | return getOpenAIVector(text, source, directories, sourceSettings.model); | 59 | return getOpenAIVector(text, source, directories, sourceSettings.model); |
| 60 | case 'openrouter': | ||
| 61 | return getOpenAIVector(text, source, directories, sourceSettings.model); | ||
| 59 | case 'transformers': | 62 | case 'transformers': |
| 60 | return getTransformersVector(text); | 63 | return getTransformersVector(text); |
| 61 | case 'extras': | 64 | case 'extras': |
| @@ -108,6 +111,9 @@ async function getBatchVector(source, sourceSettings, texts, isQuery, directorie | |||
| 108 | case 'electronhub': | 111 | case 'electronhub': |
| 109 | results.push(...await getOpenAIBatchVector(batch, source, directories, sourceSettings.model)); | 112 | results.push(...await getOpenAIBatchVector(batch, source, directories, sourceSettings.model)); |
| 110 | break; | 113 | break; |
| 114 | case 'openrouter': | ||
| 115 | results.push(...await getOpenAIBatchVector(batch, source, directories, sourceSettings.model)); | ||
| 116 | break; | ||
| 111 | case 'transformers': | 117 | case 'transformers': |
| 112 | results.push(...await getTransformersBatchVector(batch)); | 118 | results.push(...await getTransformersBatchVector(batch)); |
| 113 | break; | 119 | break; |
| @@ -166,6 +172,10 @@ function getSourceSettings(source, request) { | |||
| 166 | return { | 172 | return { |
| 167 | model: String(request.body.model || 'text-embedding-3-small'), | 173 | model: String(request.body.model || 'text-embedding-3-small'), |
| 168 | }; | 174 | }; |
| 175 | case 'openrouter': | ||
| 176 | return { | ||
| 177 | model: String(request.body.model) || 'openai/text-embedding-3-large', | ||
| 178 | }; | ||
| 169 | case 'cohere': | 179 | case 'cohere': |
| 170 | return { | 180 | return { |
| 171 | model: String(request.body.model), | 181 | model: String(request.body.model), |
| @@ -1,26 +1,37 @@ | |||
| 1 | import fetch from 'node-fetch'; | 1 | import fetch from 'node-fetch'; |
| 2 | import { SECRET_KEYS, readSecret } from '../endpoints/secrets.js'; | 2 | import { SECRET_KEYS, readSecret } from '../endpoints/secrets.js'; |
| 3 | import { OPENROUTER_HEADERS } from '../constants.js'; | ||
| 3 | 4 | ||
| 4 | const SOURCES = { | 5 | const SOURCES = { |
| 5 | 'togetherai': { | 6 | 'togetherai': { |
| 6 | secretKey: SECRET_KEYS.TOGETHERAI, | 7 | secretKey: SECRET_KEYS.TOGETHERAI, |
| 7 | url: 'api.together.xyz', | 8 | url: 'https://api.together.xyz/v1', |
| 8 | model: 'togethercomputer/m2-bert-80M-32k-retrieval', | 9 | model: 'togethercomputer/m2-bert-80M-32k-retrieval', |
| 10 | headers: {}, | ||
| 9 | }, | 11 | }, |
| 10 | 'mistral': { | 12 | 'mistral': { |
| 11 | secretKey: SECRET_KEYS.MISTRALAI, | 13 | secretKey: SECRET_KEYS.MISTRALAI, |
| 12 | url: 'api.mistral.ai', | 14 | url: 'https://api.mistral.ai/v1', |
| 13 | model: 'mistral-embed', | 15 | model: 'mistral-embed', |
| 16 | headers: {}, | ||
| 14 | }, | 17 | }, |
| 15 | 'openai': { | 18 | 'openai': { |
| 16 | secretKey: SECRET_KEYS.OPENAI, | 19 | secretKey: SECRET_KEYS.OPENAI, |
| 17 | url: 'api.openai.com', | 20 | url: 'https://api.openai.com/v1', |
| 18 | model: 'text-embedding-ada-002', | 21 | model: 'text-embedding-ada-002', |
| 22 | headers: {}, | ||
| 19 | }, | 23 | }, |
| 20 | 'electronhub': { | 24 | 'electronhub': { |
| 21 | secretKey: SECRET_KEYS.ELECTRONHUB, | 25 | secretKey: SECRET_KEYS.ELECTRONHUB, |
| 22 | url: 'api.electronhub.ai', | 26 | url: 'https://api.electronhub.ai/v1', |
| 23 | model: 'text-embedding-3-small', | 27 | model: 'text-embedding-3-small', |
| 28 | headers: {}, | ||
| 29 | }, | ||
| 30 | 'openrouter': { | ||
| 31 | secretKey: SECRET_KEYS.OPENROUTER, | ||
| 32 | url: 'https://openrouter.ai/api/v1', | ||
| 33 | model: 'openai/text-embedding-3-large', | ||
| 34 | headers: { ...OPENROUTER_HEADERS }, | ||
| 24 | }, | 35 | }, |
| 25 | }; | 36 | }; |
| 26 | 37 | ||
| @@ -48,11 +59,12 @@ export async function getOpenAIBatchVector(texts, source, directories, model = ' | |||
| 48 | } | 59 | } |
| 49 | 60 | ||
| 50 | const url = config.url; | 61 | const url = config.url; |
| 51 | const response = await fetch(`https://${url}/v1/embeddings`, { | 62 | const response = await fetch(`${url}/embeddings`, { |
| 52 | method: 'POST', | 63 | method: 'POST', |
| 53 | headers: { | 64 | headers: { |
| 54 | 'Content-Type': 'application/json', | 65 | 'Content-Type': 'application/json', |
| 55 | Authorization: `Bearer ${key}`, | 66 | 'Authorization': `Bearer ${key}`, |
| 67 | ...config.headers, | ||
| 56 | }, | 68 | }, |
| 57 | body: JSON.stringify({ | 69 | body: JSON.stringify({ |
| 58 | input: texts, | 70 | input: texts, |