feat: add Workers AI text embeddings and multimodal captioning (#5414) * feat: add Workers AI text embeddings and multimodal captioning Extends the Cloudflare Workers AI integration to the vectors and caption extensions. Embeddings: adds workers_ai source to the vectors extension using the OpenAI-compatible /v1/embeddings endpoint, with dynamic model listing from the Cloudflare model search API. Captioning: adds workers_ai as a multimodal caption API with dynamic vision model discovery via the multimodal-models endpoint. * Add logo svg * Refactor caption dropdown population * Fix order of sources * feat: add error handling for missing Workers AI account ID --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>

a9c377c3c8e70e48b1f1e1f03b00b9a0f3f67157

Tony Gies <tgies@tgies.net>

Signed
10 files changed, +211 -3Ignore whitespace
public/img/workers_ai.svg+9 -0
@@ -0,0 +1,9 @@
1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="64" height="64">
2 <path d="M8.16 23h21.177v-5.86l-4.023-2.307-.694-.3-16.46.113z" fill="none" />
3 <path
4 d="M22.012 22.222c.197-.675.122-1.294-.206-1.754-.3-.422-.807-.666-1.416-.694l-11.545-.15c-.075 0-.14-.038-.178-.094s-.047-.13-.028-.206c.038-.113.15-.197.272-.206l11.648-.15c1.38-.066 2.88-1.182 3.404-2.55l.666-1.735a.38.38 0 0 0 .02-.225c-.75-3.395-3.78-5.927-7.4-5.927-3.34 0-6.17 2.157-7.184 5.15-.657-.488-1.5-.75-2.392-.666-1.604.16-2.9 1.444-3.048 3.048a3.58 3.58 0 0 0 .084 1.191A4.84 4.84 0 0 0 0 22.1c0 .234.02.47.047.703.02.113.113.197.225.197H21.58a.29.29 0 0 0 .272-.206l.16-.572z"
5 />
6 <path
7 d="M25.688 14.803l-.32.01c-.075 0-.14.056-.17.13l-.45 1.566c-.197.675-.122 1.294.206 1.754.3.422.807.666 1.416.694l2.457.15c.075 0 .14.038.178.094s.047.14.028.206c-.038.113-.15.197-.272.206l-2.56.15c-1.388.066-2.88 1.182-3.404 2.55l-.188.478c-.038.094.028.188.13.188h8.797a.23.23 0 0 0 .225-.169A6.41 6.41 0 0 0 32 21.106a6.32 6.32 0 0 0-6.312-6.302"
8 />
9</svg>
\ No newline at end of file9 \ No newline at end of file
public/scripts/extensions/caption/index.js+6 -2
@@ -3,6 +3,7 @@ import { getContext, getApiUrl, doExtrasFetch, extension_settings, modules, rend
3import { appendMediaToMessage, chat_metadata, eventSource, event_types, getRequestHeaders, saveChatConditional, saveSettingsDebounced, substituteParamsExtended } from '../../../script.js';3import { appendMediaToMessage, chat_metadata, eventSource, event_types, getRequestHeaders, saveChatConditional, saveSettingsDebounced, substituteParamsExtended } from '../../../script.js';
4import { getMessageTimeStamp } from '../../RossAscends-mods.js';4import { getMessageTimeStamp } from '../../RossAscends-mods.js';
5import { SECRET_KEYS, secret_state } from '../../secrets.js';5import { SECRET_KEYS, secret_state } from '../../secrets.js';
6import { oai_settings } from '../../openai.js';
6import { getMultimodalCaption } from '../shared.js';7import { getMultimodalCaption } from '../shared.js';
7import { textgen_types, textgenerationwebui_settings } from '../../textgen-settings.js';8import { textgen_types, textgenerationwebui_settings } from '../../textgen-settings.js';
8import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';9import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
@@ -504,6 +505,7 @@ jQuery(async function () {
504 'chutes': SECRET_KEYS.CHUTES,505 'chutes': SECRET_KEYS.CHUTES,
505 'electronhub': SECRET_KEYS.ELECTRONHUB,506 'electronhub': SECRET_KEYS.ELECTRONHUB,
506 'pollinations': SECRET_KEYS.POLLINATIONS,507 'pollinations': SECRET_KEYS.POLLINATIONS,
508 'workers_ai': SECRET_KEYS.WORKERS_AI,
507 };509 };
508510
509 if (chatCompletionApis[api] && secret_state[chatCompletionApis[api]]) {511 if (chatCompletionApis[api] && secret_state[chatCompletionApis[api]]) {
@@ -580,7 +582,7 @@ jQuery(async function () {
580 }582 }
581583
582 async function addRemoteEndpointModels() {584 async function addRemoteEndpointModels() {
583 async function processEndpoint(api, url) {585 async function processEndpoint(api, url, additionalParams = {}) {
584 const dropdown = document.getElementById('caption_multimodal_model');586 const dropdown = document.getElementById('caption_multimodal_model');
585 if (!(dropdown instanceof HTMLSelectElement)) {587 if (!(dropdown instanceof HTMLSelectElement)) {
586 return;588 return;
@@ -591,7 +593,8 @@ jQuery(async function () {
591 const options = Array.from(dropdown.options);593 const options = Array.from(dropdown.options);
592 const response = await fetch(url, {594 const response = await fetch(url, {
593 method: 'POST',595 method: 'POST',
594 headers: getRequestHeaders({ omitContentType: true }),596 headers: getRequestHeaders(),
597 body: JSON.stringify(additionalParams),
595 });598 });
596 if (!response.ok) {599 if (!response.ok) {
597 return;600 return;
@@ -620,6 +623,7 @@ jQuery(async function () {
620 await processEndpoint('mistral', '/api/backends/chat-completions/multimodal-models/mistral');623 await processEndpoint('mistral', '/api/backends/chat-completions/multimodal-models/mistral');
621 await processEndpoint('xai', '/api/backends/chat-completions/multimodal-models/xai');624 await processEndpoint('xai', '/api/backends/chat-completions/multimodal-models/xai');
622 await processEndpoint('moonshot', '/api/backends/chat-completions/multimodal-models/moonshot');625 await processEndpoint('moonshot', '/api/backends/chat-completions/multimodal-models/moonshot');
626 await processEndpoint('workers_ai', '/api/backends/chat-completions/multimodal-models/workers_ai', { workers_ai_account_id: oai_settings.workers_ai_account_id });
623 }627 }
624628
625 await addSettings();629 await addSettings();
public/scripts/extensions/caption/settings.html+1 -0
@@ -20,6 +20,7 @@
20 <option value="aimlapi">AI/ML API</option>20 <option value="aimlapi">AI/ML API</option>
21 <option value="chutes">Chutes</option>21 <option value="chutes">Chutes</option>
22 <option value="anthropic">Claude</option>22 <option value="anthropic">Claude</option>
23 <option value="workers_ai">Cloudflare Workers AI</option>
23 <option value="cohere">Cohere</option>24 <option value="cohere">Cohere</option>
24 <option value="custom" data-i18n="Custom (OpenAI-compatible)">Custom (OpenAI-compatible)</option>25 <option value="custom" data-i18n="Custom (OpenAI-compatible)">Custom (OpenAI-compatible)</option>
25 <option value="electronhub">Electron Hub</option>26 <option value="electronhub">Electron Hub</option>
public/scripts/extensions/shared.js+8 -0
@@ -126,6 +126,10 @@ export async function getMultimodalCaption(base64Img, prompt) {
126 requestBody.zai_endpoint = oai_settings.zai_endpoint || ZAI_ENDPOINT.COMMON;126 requestBody.zai_endpoint = oai_settings.zai_endpoint || ZAI_ENDPOINT.COMMON;
127 }127 }
128128
129 if (extension_settings.caption.multimodal_api === 'workers_ai') {
130 requestBody.workers_ai_account_id = oai_settings.workers_ai_account_id;
131 }
132
129 function getEndpointUrl() {133 function getEndpointUrl() {
130 switch (extension_settings.caption.multimodal_api) {134 switch (extension_settings.caption.multimodal_api) {
131 case 'google':135 case 'google':
@@ -283,6 +287,10 @@ function throwIfInvalidModel(useReverseProxy) {
283 if (multimodalApi === 'pollinations' && !secret_state[SECRET_KEYS.POLLINATIONS]) {287 if (multimodalApi === 'pollinations' && !secret_state[SECRET_KEYS.POLLINATIONS]) {
284 throw new Error('Pollinations API key is not set.');288 throw new Error('Pollinations API key is not set.');
285 }289 }
290
291 if (multimodalApi === 'workers_ai' && (!secret_state[SECRET_KEYS.WORKERS_AI] || !oai_settings.workers_ai_account_id)) {
292 throw new Error('Workers AI API key or account ID is not set.');
293 }
286}294}
287295
288/**296/**
public/scripts/extensions/vectors/index.js+59 -0
@@ -389,6 +389,8 @@ async function synchronizeChat(batchSize = 5) {
389 return 'Extras API must provide an "embeddings" module.';389 return 'Extras API must provide an "embeddings" module.';
390 case 'webllm_not_supported':390 case 'webllm_not_supported':
391 return 'WebLLM extension is not installed or the model is not set.';391 return 'WebLLM extension is not installed or the model is not set.';
392 case 'account_id_missing':
393 return 'Workers AI account ID is required. Save it in the "API Connections" panel.';
392 default:394 default:
393 return 'Check server console for more details';395 return 'Check server console for more details';
394 }396 }
@@ -843,6 +845,10 @@ function getVectorsRequestBody(args = {}) {
843 body.model = extension_settings.vectors.siliconflow_model;845 body.model = extension_settings.vectors.siliconflow_model;
844 body.siliconflow_endpoint = oai_settings.siliconflow_endpoint;846 body.siliconflow_endpoint = oai_settings.siliconflow_endpoint;
845 break;847 break;
848 case 'workers_ai':
849 body.model = extension_settings.vectors.workers_ai_model || '@cf/baai/bge-m3';
850 body.workers_ai_account_id = oai_settings.workers_ai_account_id;
851 break;
846 default:852 default:
847 break;853 break;
848 }854 }
@@ -936,6 +942,7 @@ function throwIfSourceInvalid() {
936 settings.source === 'togetherai' && !secret_state[SECRET_KEYS.TOGETHERAI] ||942 settings.source === 'togetherai' && !secret_state[SECRET_KEYS.TOGETHERAI] ||
937 settings.source === 'nomicai' && !secret_state[SECRET_KEYS.NOMICAI] ||943 settings.source === 'nomicai' && !secret_state[SECRET_KEYS.NOMICAI] ||
938 settings.source === 'cohere' && !secret_state[SECRET_KEYS.COHERE] ||944 settings.source === 'cohere' && !secret_state[SECRET_KEYS.COHERE] ||
945 settings.source === 'workers_ai' && !secret_state[SECRET_KEYS.WORKERS_AI] ||
939 settings.source === 'siliconflow' && !secret_state[SECRET_KEYS.SILICONFLOW]) {946 settings.source === 'siliconflow' && !secret_state[SECRET_KEYS.SILICONFLOW]) {
940 throw new Error('Vectors: API key missing', { cause: 'api_key_missing' });947 throw new Error('Vectors: API key missing', { cause: 'api_key_missing' });
941 }948 }
@@ -964,6 +971,10 @@ function throwIfSourceInvalid() {
964 if (settings.source === 'webllm' && (!isWebLlmSupported() || !settings.webllm_model)) {971 if (settings.source === 'webllm' && (!isWebLlmSupported() || !settings.webllm_model)) {
965 throw new Error('Vectors: WebLLM is not supported', { cause: 'webllm_not_supported' });972 throw new Error('Vectors: WebLLM is not supported', { cause: 'webllm_not_supported' });
966 }973 }
974
975 if (settings.source === 'workers_ai' && !oai_settings.workers_ai_account_id) {
976 throw new Error('Vectors: Workers AI account ID missing', { cause: 'account_id_missing' });
977 }
967}978}
968979
969/**980/**
@@ -1155,6 +1166,7 @@ function toggleSettings() {
1155 $('#koboldcpp_vectorsModel').toggle(settings.source === 'koboldcpp');1166 $('#koboldcpp_vectorsModel').toggle(settings.source === 'koboldcpp');
1156 $('#google_vectorsModel').toggle(settings.source === 'palm' || settings.source === 'vertexai');1167 $('#google_vectorsModel').toggle(settings.source === 'palm' || settings.source === 'vertexai');
1157 $('#siliconflow_vectorsModel').toggle(settings.source === 'siliconflow');1168 $('#siliconflow_vectorsModel').toggle(settings.source === 'siliconflow');
1169 $('#workers_ai_vectorsModel').toggle(settings.source === 'workers_ai');
1158 $('#vector_altEndpointUrl').toggle(vectorApiRequiresUrl.includes(settings.source));1170 $('#vector_altEndpointUrl').toggle(vectorApiRequiresUrl.includes(settings.source));
1159 switch (settings.source) {1171 switch (settings.source) {
1160 case 'webllm':1172 case 'webllm':
@@ -1175,6 +1187,9 @@ function toggleSettings() {
1175 case 'siliconflow':1187 case 'siliconflow':
1176 loadSiliconFlowModels();1188 loadSiliconFlowModels();
1177 break;1189 break;
1190 case 'workers_ai':
1191 loadWorkersAIModels();
1192 break;
1178 }1193 }
1179}1194}
11801195
@@ -1362,6 +1377,45 @@ function populateSiliconFlowModelSelect(models) {
1362 $('#vectors_siliconflow_model').val(settings.siliconflow_model);1377 $('#vectors_siliconflow_model').val(settings.siliconflow_model);
1363}1378}
13641379
1380async function loadWorkersAIModels() {
1381 try {
1382 const response = await fetch('/api/openai/workers-ai/models/embedding', {
1383 method: 'POST',
1384 headers: getRequestHeaders(),
1385 body: JSON.stringify({
1386 workers_ai_account_id: oai_settings.workers_ai_account_id,
1387 }),
1388 });
1389 if (!response.ok) {
1390 throw new Error(`HTTP ${response.status}`);
1391 }
1392 /** @type {Array<any>} */
1393 const data = await response.json();
1394 const models = Array.isArray(data) ? data : [];
1395 populateWorkersAIModelSelect(models);
1396 } catch (err) {
1397 console.warn('Workers AI models fetch failed', err);
1398 populateWorkersAIModelSelect([]);
1399 }
1400}
1401
1402function populateWorkersAIModelSelect(models) {
1403 const select = $('#vectors_workers_ai_model');
1404 select.empty();
1405 for (const m of models) {
1406 const option = document.createElement('option');
1407 option.value = m.id;
1408 option.text = m.id;
1409 select.append(option);
1410 }
1411 if (!settings.workers_ai_model && models.length) {
1412 settings.workers_ai_model = models[0].id;
1413 Object.assign(extension_settings.vectors, settings);
1414 saveSettingsDebounced();
1415 }
1416 $('#vectors_workers_ai_model').val(settings.workers_ai_model);
1417}
1418
1365/**1419/**
1366 * Executes a function with WebLLM error handling.1420 * Executes a function with WebLLM error handling.
1367 * @param {function(): Promise<T>} func Function to execute1421 * @param {function(): Promise<T>} func Function to execute
@@ -1785,6 +1839,11 @@ jQuery(async () => {
1785 Object.assign(extension_settings.vectors, settings);1839 Object.assign(extension_settings.vectors, settings);
1786 saveSettingsDebounced();1840 saveSettingsDebounced();
1787 });1841 });
1842 $('#vectors_workers_ai_model').val(settings.workers_ai_model).on('change', () => {
1843 settings.workers_ai_model = String($('#vectors_workers_ai_model').val());
1844 Object.assign(extension_settings.vectors, settings);
1845 saveSettingsDebounced();
1846 });
1788 $('#vectors_openrouter_model').val(settings.openrouter_model).on('change', () => {1847 $('#vectors_openrouter_model').val(settings.openrouter_model).on('change', () => {
1789 settings.openrouter_model = String($('#vectors_openrouter_model').val());1848 settings.openrouter_model = String($('#vectors_openrouter_model').val());
1790 Object.assign(extension_settings.vectors, settings);1849 Object.assign(extension_settings.vectors, settings);
public/scripts/extensions/vectors/settings.html+11 -0
@@ -11,6 +11,7 @@
11 </label>11 </label>
12 <select id="vectors_source" class="text_pole">12 <select id="vectors_source" class="text_pole">
13 <option value="chutes">Chutes</option>13 <option value="chutes">Chutes</option>
14 <option value="workers_ai">Cloudflare Workers AI</option>
14 <option value="cohere">Cohere</option>15 <option value="cohere">Cohere</option>
15 <option value="electronhub">Electron Hub</option>16 <option value="electronhub">Electron Hub</option>
16 <option value="extras">Extras (deprecated)</option>17 <option value="extras">Extras (deprecated)</option>
@@ -205,6 +206,16 @@
205 </i>206 </i>
206 </div>207 </div>
207208
209 <div class="flex-container flexFlowColumn" id="workers_ai_vectorsModel">
210 <label for="vectors_workers_ai_model" data-i18n="Vectorization Model">
211 Vectorization Model
212 </label>
213 <select id="vectors_workers_ai_model" class="text_pole"></select>
214 <i data-i18n="Hint: Set your Workers AI API key and Account ID in API Connections.">
215 Hint: Set your Workers AI API key and Account ID in API Connections.
216 </i>
217 </div>
218
208 <div class="flex-container marginTopBot5">219 <div class="flex-container marginTopBot5">
209 <div class="flex-container flex1 flexFlowColumn" title="How many last messages will be matched for relevance.">220 <div class="flex-container flex1 flexFlowColumn" title="How many last messages will be matched for relevance.">
210 <label for="vectors_query">221 <label for="vectors_query">
src/endpoints/backends/chat-completions.js+33 -0
@@ -2724,6 +2724,39 @@ multimodalModels.post('/moonshot', async (req, res) => {
2724 }2724 }
2725});2725});
27262726
2727multimodalModels.post('/workers_ai', async (req, res) => {
2728 try {
2729 const key = readSecret(req.user.directories, SECRET_KEYS.WORKERS_AI);
2730 const accountId = String(req.body.workers_ai_account_id || '').trim();
2731
2732 if (!key || !accountId) {
2733 return res.json([]);
2734 }
2735
2736 const apiUrl = `https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(accountId)}/ai/models/search?task=Text+Generation&per_page=1000`;
2737 const response = await fetch(apiUrl, {
2738 method: 'GET',
2739 headers: { 'Authorization': 'Bearer ' + key },
2740 });
2741
2742 if (!response.ok) {
2743 return res.json([]);
2744 }
2745
2746 /** @type {any} */
2747 const data = await response.json();
2748 const models = Array.isArray(data?.result)
2749 ? data.result
2750 .filter(m => Array.isArray(m.properties) && m.properties.some(p => p.property_id === 'vision' && p.value === 'true'))
2751 .map(m => m.name)
2752 : [];
2753 return res.json(models);
2754 } catch (error) {
2755 console.error(error);
2756 return res.sendStatus(500);
2757 }
2758});
2759
2727router.use('/multimodal-models', multimodalModels);2760router.use('/multimodal-models', multimodalModels);
27282761
2729router.post('/process', async function (request, response) {2762router.post('/process', async function (request, response) {
src/endpoints/openai.js+56 -0
@@ -102,6 +102,10 @@ router.post('/caption-image', async (request, response) => {
102 bodyParams.seed = Math.floor(Math.random() * Math.pow(2, 32));102 bodyParams.seed = Math.floor(Math.random() * Math.pow(2, 32));
103 }103 }
104104
105 if (request.body.api === 'workers_ai') {
106 key = readSecret(request.user.directories, SECRET_KEYS.WORKERS_AI);
107 }
108
105 const noKeyTypes = ['custom', 'ooba', 'koboldcpp', 'vllm', 'llamacpp'];109 const noKeyTypes = ['custom', 'ooba', 'koboldcpp', 'vllm', 'llamacpp'];
106 if (!key && !request.body.reverse_proxy && !noKeyTypes.includes(request.body.api)) {110 if (!key && !request.body.reverse_proxy && !noKeyTypes.includes(request.body.api)) {
107 console.warn('No key found for API', request.body.api);111 console.warn('No key found for API', request.body.api);
@@ -216,6 +220,14 @@ router.post('/caption-image', async (request, response) => {
216 }220 }
217 }221 }
218222
223 if (request.body.api === 'workers_ai') {
224 const accountId = String(request.body.workers_ai_account_id || '').trim();
225 if (!accountId) {
226 return response.status(400).send({ error: 'Cloudflare Workers AI Account ID is required' });
227 }
228 apiUrl = `https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(accountId)}/ai/v1/chat/completions`;
229 }
230
219 if (['koboldcpp', 'vllm', 'llamacpp', 'ooba'].includes(request.body.api)) {231 if (['koboldcpp', 'vllm', 'llamacpp', 'ooba'].includes(request.body.api)) {
220 apiUrl = `${trimV1(request.body.server_url)}/v1/chat/completions`;232 apiUrl = `${trimV1(request.body.server_url)}/v1/chat/completions`;
221 }233 }
@@ -570,6 +582,50 @@ router.post('/siliconflow/models/embedding', async (request, response) => {
570 }582 }
571});583});
572584
585router.post('/workers-ai/models/embedding', async (request, response) => {
586 try {
587 const key = readSecret(request.user.directories, SECRET_KEYS.WORKERS_AI);
588
589 if (!key) {
590 console.warn('No Workers AI key found');
591 return response.sendStatus(400);
592 }
593
594 const accountId = String(request.body.workers_ai_account_id || '').trim();
595 if (!accountId) {
596 console.warn('No Workers AI account ID found');
597 return response.sendStatus(400);
598 }
599
600 const apiUrl = `https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(accountId)}/ai/models/search?task=Text+Embeddings&per_page=100`;
601 const result = await fetch(apiUrl, {
602 method: 'GET',
603 headers: {
604 Authorization: `Bearer ${key}`,
605 },
606 });
607
608 if (!result.ok) {
609 const text = await result.text();
610 console.warn('Workers AI embedding models request failed', result.statusText, text);
611 return response.status(500).send(text);
612 }
613
614 /** @type {any} */
615 const data = await result.json();
616
617 if (!Array.isArray(data?.result)) {
618 console.warn('Workers AI embedding models response invalid', data);
619 return response.sendStatus(500);
620 }
621
622 return response.json(data.result.map(m => ({ ...m, id: m.name })));
623 } catch (error) {
624 console.error('Workers AI embedding models fetch failed', error);
625 response.sendStatus(500);
626 }
627});
628
573router.post('/generate-image', async (request, response) => {629router.post('/generate-image', async (request, response) => {
574 try {630 try {
575 const key = readSecret(request.user.directories, SECRET_KEYS.OPENAI);631 const key = readSecret(request.user.directories, SECRET_KEYS.OPENAI);
src/endpoints/vectors.js+15 -0
@@ -39,6 +39,7 @@ const SOURCES = [
39 'chutes',39 'chutes',
40 'nanogpt',40 'nanogpt',
41 'siliconflow',41 'siliconflow',
42 'workers_ai',
42];43];
4344
44/**45/**
@@ -88,6 +89,8 @@ async function getVector(source, sourceSettings, text, isQuery, directories) {
88 return getOpenAIVector(text, source, directories, sourceSettings.model);89 return getOpenAIVector(text, source, directories, sourceSettings.model);
89 case 'siliconflow':90 case 'siliconflow':
90 return getOpenAIVector(text, source, directories, sourceSettings.model, sourceSettings.urlOverride);91 return getOpenAIVector(text, source, directories, sourceSettings.model, sourceSettings.urlOverride);
92 case 'workers_ai':
93 return getOpenAIVector(text, source, directories, sourceSettings.model, sourceSettings.urlOverride);
91 }94 }
9295
93 throw new Error(`Unknown vector source ${source}`);96 throw new Error(`Unknown vector source ${source}`);
@@ -162,6 +165,9 @@ async function getBatchVector(source, sourceSettings, texts, isQuery, directorie
162 case 'siliconflow':165 case 'siliconflow':
163 results.push(...await getOpenAIBatchVector(batch, source, directories, sourceSettings.model, sourceSettings.urlOverride));166 results.push(...await getOpenAIBatchVector(batch, source, directories, sourceSettings.model, sourceSettings.urlOverride));
164 break;167 break;
168 case 'workers_ai':
169 results.push(...await getOpenAIBatchVector(batch, source, directories, sourceSettings.model, sourceSettings.urlOverride));
170 break;
165 default:171 default:
166 throw new Error(`Unknown vector source ${source}`);172 throw new Error(`Unknown vector source ${source}`);
167 }173 }
@@ -260,6 +266,15 @@ function getSourceSettings(source, request) {
260 urlOverride: request.body.siliconflow_endpoint === 'cn'266 urlOverride: request.body.siliconflow_endpoint === 'cn'
261 ? 'https://api.siliconflow.cn/v1' : null,267 ? 'https://api.siliconflow.cn/v1' : null,
262 };268 };
269 case 'workers_ai': {
270 const accountId = String(request.body.workers_ai_account_id || '').trim();
271 return {
272 model: String(request.body.model || '@cf/baai/bge-m3'),
273 urlOverride: accountId
274 ? `https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(accountId)}/ai/v1`
275 : null,
276 };
277 }
263 default:278 default:
264 return {};279 return {};
265 }280 }
src/vectors/openai-vectors.js+13 -1
@@ -61,6 +61,13 @@ const SOURCES = {
61 headers: {},61 headers: {},
62 processBody: () => {},62 processBody: () => {},
63 },63 },
64 'workers_ai': {
65 secretKey: SECRET_KEYS.WORKERS_AI,
66 url: '', // Constructed at runtime from account ID via urlOverride
67 model: '@cf/baai/bge-m3',
68 headers: {},
69 processBody: () => {},
70 },
64};71};
6572
66/**73/**
@@ -88,7 +95,12 @@ export async function getOpenAIBatchVector(texts, source, directories, model = '
88 }95 }
8996
90 const modelName = model || config.model;97 const modelName = model || config.model;
91 const url = urlOverride || config.url.replace('{{MODEL}}', modelName);98 const url = urlOverride || config.url?.replace('{{MODEL}}', modelName);
99
100 if (!url) {
101 throw new Error(`No API URL configured for source ${source}`);
102 }
103
92 const body = {104 const body = {
93 input: texts,105 input: texts,
94 model: modelName,106 model: modelName,