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 = {
6262 togetherai_model: 'togethercomputer/m2-bert-80M-32k-retrieval',
6363 openai_model: 'text-embedding-ada-002',
6464 electronhub_model: 'text-embedding-3-small',
65+ openrouter_model: 'openai/text-embedding-3-large',
6566 cohere_model: 'embed-english-v3.0',
6667 ollama_model: 'mxbai-embed-large',
6768 ollama_keep: false,
@@ -790,6 +791,9 @@ function getVectorsRequestBody(args = {}) {
790791 case 'electronhub':
791792 body.model = extension_settings.vectors.electronhub_model;
792793 break;
794+ case 'openrouter':
795+ body.model = extension_settings.vectors.openrouter_model;
796+ break;
793797 case 'togetherai':
794798 body.model = extension_settings.vectors.togetherai_model;
795799 break;
@@ -909,6 +913,7 @@ async function insertVectorItems(collectionId, items) {
909913function throwIfSourceInvalid() {
910914 if (settings.source === 'openai' && !secret_state[SECRET_KEYS.OPENAI] ||
911915 settings.source === 'electronhub' && !secret_state[SECRET_KEYS.ELECTRONHUB] ||
916+ settings.source === 'openrouter' && !secret_state[SECRET_KEYS.OPENROUTER] ||
912917 settings.source === 'palm' && !secret_state[SECRET_KEYS.MAKERSUITE] ||
913918 settings.source === 'vertexai' && !secret_state[SECRET_KEYS.VERTEXAI] && !secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT] ||
914919 settings.source === 'mistral' && !secret_state[SECRET_KEYS.MISTRALAI] ||
@@ -1122,6 +1127,7 @@ function toggleSettings() {
11221127 $('#together_vectorsModel').toggle(settings.source === 'togetherai');
11231128 $('#openai_vectorsModel').toggle(settings.source === 'openai');
11241129 $('#electronhub_vectorsModel').toggle(settings.source === 'electronhub');
1130+ $('#openrouter_vectorsModel').toggle(settings.source === 'openrouter');
11251131 $('#cohere_vectorsModel').toggle(settings.source === 'cohere');
11261132 $('#ollama_vectorsModel').toggle(settings.source === 'ollama');
11271133 $('#llamacpp_vectorsModel').toggle(settings.source === 'llamacpp');
@@ -1138,6 +1144,9 @@ function toggleSettings() {
11381144 case 'electronhub':
11391145 loadElectronHubModels();
11401146 break;
1147+ case 'openrouter':
1148+ loadOpenRouterModels();
1149+ break;
11411150 }
11421151}
11431152
@@ -1180,6 +1189,44 @@ function populateElectronHubModelSelect(models) {
11801189 $('#vectors_electronhub_model').val(settings.electronhub_model);
11811190}
11821191
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+
11831230/**
11841231 * Executes a function with WebLLM error handling.
11851232 * @param {function(): Promise<T>} func Function to execute
@@ -1583,6 +1630,11 @@ jQuery(async () => {
15831630 Object.assign(extension_settings.vectors, settings);
15841631 saveSettingsDebounced();
15851632 });
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+ });
15861638 $('#vectors_cohere_model').val(settings.cohere_model).on('change', () => {
15871639 settings.cohere_model = String($('#vectors_cohere_model').val());
15881640 Object.assign(extension_settings.vectors, settings);
public/scripts/extensions/vectors/settings.html+10 -0
@@ -22,6 +22,7 @@
2222 <option value="nomicai">NomicAI</option>
2323 <option value="ollama">Ollama</option>
2424 <option value="openai">OpenAI</option>
25+ <option value="openrouter">OpenRouter</option>
2526 <option value="togetherai">TogetherAI</option>
2627 <option value="vllm">vLLM</option>
2728 <option value="webllm" data-i18n="WebLLM Extension">WebLLM Extension</option>
@@ -163,6 +164,15 @@
163164 <span data-i18n="Click to set">Click to set</span>
164165 </div>
165166 </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
167177 <div class="flex-container marginTopBot5">
168178 <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) => {
5555 return res.sendStatus(500);
5656 }
5757});
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+});
src/endpoints/vectors.js+10 -0
@@ -35,6 +35,7 @@ const SOURCES = [
3535 'koboldcpp',
3636 'vertexai',
3737 'electronhub',
38+ 'openrouter',
3839];
3940
4041/**
@@ -56,6 +57,8 @@ async function getVector(source, sourceSettings, text, isQuery, directories) {
5657 return getOpenAIVector(text, source, directories, sourceSettings.model);
5758 case 'electronhub':
5859 return getOpenAIVector(text, source, directories, sourceSettings.model);
60+ case 'openrouter':
61+ return getOpenAIVector(text, source, directories, sourceSettings.model);
5962 case 'transformers':
6063 return getTransformersVector(text);
6164 case 'extras':
@@ -108,6 +111,9 @@ async function getBatchVector(source, sourceSettings, texts, isQuery, directorie
108111 case 'electronhub':
109112 results.push(...await getOpenAIBatchVector(batch, source, directories, sourceSettings.model));
110113 break;
114+ case 'openrouter':
115+ results.push(...await getOpenAIBatchVector(batch, source, directories, sourceSettings.model));
116+ break;
111117 case 'transformers':
112118 results.push(...await getTransformersBatchVector(batch));
113119 break;
@@ -166,6 +172,10 @@ function getSourceSettings(source, request) {
166172 return {
167173 model: String(request.body.model || 'text-embedding-3-small'),
168174 };
175+ case 'openrouter':
176+ return {
177+ model: String(request.body.model) || 'openai/text-embedding-3-large',
178+ };
169179 case 'cohere':
170180 return {
171181 model: String(request.body.model),
src/vectors/openai-vectors.js+18 -6
@@ -1,26 +1,37 @@
11import fetch from 'node-fetch';
22import { SECRET_KEYS, readSecret } from '../endpoints/secrets.js';
3+import { OPENROUTER_HEADERS } from '../constants.js';
34
45const SOURCES = {
56 'togetherai': {
67 secretKey: SECRET_KEYS.TOGETHERAI,
78 url: 'https://api.together.xyz/v1',
89 model: 'togethercomputer/m2-bert-80M-32k-retrieval',
10+ headers: {},
911 },
1012 'mistral': {
1113 secretKey: SECRET_KEYS.MISTRALAI,
1214 url: 'https://api.mistral.ai/v1',
1315 model: 'mistral-embed',
16+ headers: {},
1417 },
1518 'openai': {
1619 secretKey: SECRET_KEYS.OPENAI,
1720 url: 'https://api.openai.com/v1',
1821 model: 'text-embedding-ada-002',
22+ headers: {},
1923 },
2024 'electronhub': {
2125 secretKey: SECRET_KEYS.ELECTRONHUB,
2226 url: 'https://api.electronhub.ai/v1',
2327 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 },
2435 },
2536};
2637
@@ -48,11 +59,12 @@ export async function getOpenAIBatchVector(texts, source, directories, model = '
4859 }
4960
5061 const url = config.url;
5162 const response = await fetch(`https://${url}/v1/embeddings`, {
5263 method: 'POST',
5364 headers: {
5465 'Content-Type': 'application/json',
5566 'Authorization': `Bearer ${key}`,
67+ ...config.headers,
5668 },
5769 body: JSON.stringify({
5870 input: texts,