OpenRouter: Add as vector storage provider (#4736) * OpenRouter: Add as vector storage provider * Better array filtering

845bd8c99980451a02e4e58643e2efb56fd89c37

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
5 files changed, +124 -6Showing whitespace changes
public/scripts/extensions/vectors/index.js+52 -0
@@ -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) {
909function throwIfSourceInvalid() {913function 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}
11431152
@@ -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}
11821191
1192async 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 */
1215function 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 execute1232 * @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);
public/scripts/extensions/vectors/settings.html+10 -0
@@ -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>
166176
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.">
src/endpoints/openrouter.js+34 -0
@@ -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
59router.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});
src/endpoints/vectors.js+10 -0
@@ -35,6 +35,7 @@ const SOURCES = [
35 'koboldcpp',35 'koboldcpp',
36 'vertexai',36 'vertexai',
37 'electronhub',37 'electronhub',
38 'openrouter',
38];39];
3940
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),
src/vectors/openai-vectors.js+18 -6
@@ -1,26 +1,37 @@
1import fetch from 'node-fetch';1import fetch from 'node-fetch';
2import { SECRET_KEYS, readSecret } from '../endpoints/secrets.js';2import { SECRET_KEYS, readSecret } from '../endpoints/secrets.js';
3import { OPENROUTER_HEADERS } from '../constants.js';
34
4const SOURCES = {5const 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};
2637
@@ -48,11 +59,12 @@ export async function getOpenAIBatchVector(texts, source, directories, model = '
48 }59 }
4960
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,