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>
Signed| @@ -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 file | 9 | \ No newline at end of file | |
| @@ -3,6 +3,7 @@ import { getContext, getApiUrl, doExtrasFetch, extension_settings, modules, rend | |||
| 3 | import { appendMediaToMessage, chat_metadata, eventSource, event_types, getRequestHeaders, saveChatConditional, saveSettingsDebounced, substituteParamsExtended } from '../../../script.js'; | 3 | import { appendMediaToMessage, chat_metadata, eventSource, event_types, getRequestHeaders, saveChatConditional, saveSettingsDebounced, substituteParamsExtended } from '../../../script.js'; |
| 4 | import { getMessageTimeStamp } from '../../RossAscends-mods.js'; | 4 | import { getMessageTimeStamp } from '../../RossAscends-mods.js'; |
| 5 | import { SECRET_KEYS, secret_state } from '../../secrets.js'; | 5 | import { SECRET_KEYS, secret_state } from '../../secrets.js'; |
| 6 | import { oai_settings } from '../../openai.js'; | ||
| 6 | import { getMultimodalCaption } from '../shared.js'; | 7 | import { getMultimodalCaption } from '../shared.js'; |
| 7 | import { textgen_types, textgenerationwebui_settings } from '../../textgen-settings.js'; | 8 | import { textgen_types, textgenerationwebui_settings } from '../../textgen-settings.js'; |
| 8 | import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; | 9 | import { 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 | }; |
| 508 | 510 | ||
| 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 | } |
| 581 | 583 | ||
| 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 | } |
| 624 | 628 | ||
| 625 | await addSettings(); | 629 | await addSettings(); |
| @@ -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> |
| @@ -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 | } |
| 128 | 128 | ||
| 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 | } |
| 287 | 295 | ||
| 288 | /** | 296 | /** |
| @@ -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 | } |
| 968 | 979 | ||
| 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 | } |
| 1180 | 1195 | ||
| @@ -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 | } |
| 1364 | 1379 | ||
| 1380 | async 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 | |||
| 1402 | function 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 execute | 1421 | * @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); |
| @@ -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> |
| 207 | 208 | ||
| 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"> |
| @@ -2724,6 +2724,39 @@ multimodalModels.post('/moonshot', async (req, res) => { | |||
| 2724 | } | 2724 | } |
| 2725 | }); | 2725 | }); |
| 2726 | 2726 | ||
| 2727 | multimodalModels.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 | |||
| 2727 | router.use('/multimodal-models', multimodalModels); | 2760 | router.use('/multimodal-models', multimodalModels); |
| 2728 | 2761 | ||
| 2729 | router.post('/process', async function (request, response) { | 2762 | router.post('/process', async function (request, response) { |
| @@ -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 | } |
| 104 | 104 | ||
| 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 | } |
| 218 | 222 | ||
| 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 | }); |
| 572 | 584 | ||
| 585 | router.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 | |||
| 573 | router.post('/generate-image', async (request, response) => { | 629 | router.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); |
| @@ -39,6 +39,7 @@ const SOURCES = [ | |||
| 39 | 'chutes', | 39 | 'chutes', |
| 40 | 'nanogpt', | 40 | 'nanogpt', |
| 41 | 'siliconflow', | 41 | 'siliconflow', |
| 42 | 'workers_ai', | ||
| 42 | ]; | 43 | ]; |
| 43 | 44 | ||
| 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 | } |
| 92 | 95 | ||
| 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 | } |
| @@ -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 | }; |
| 65 | 72 | ||
| 66 | /** | 73 | /** |
| @@ -88,7 +95,12 @@ export async function getOpenAIBatchVector(texts, source, directories, model = ' | |||
| 88 | } | 95 | } |
| 89 | 96 | ||
| 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, |