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> | |
| 9 | \ No newline at end of file | |
| @@ -3,6 +3,7 @@ import { getContext, getApiUrl, doExtrasFetch, extension_settings, modules, rend | ||
| 3 | 3 | import { appendMediaToMessage, chat_metadata, eventSource, event_types, getRequestHeaders, saveChatConditional, saveSettingsDebounced, substituteParamsExtended } from '../../../script.js'; |
| 4 | 4 | import { getMessageTimeStamp } from '../../RossAscends-mods.js'; |
| 5 | 5 | import { SECRET_KEYS, secret_state } from '../../secrets.js'; |
| 6 | +import { oai_settings } from '../../openai.js'; | |
| 6 | 7 | import { getMultimodalCaption } from '../shared.js'; |
| 7 | 8 | import { textgen_types, textgenerationwebui_settings } from '../../textgen-settings.js'; |
| 8 | 9 | import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; |
| @@ -504,6 +505,7 @@ jQuery(async function () { | ||
| 504 | 505 | 'chutes': SECRET_KEYS.CHUTES, |
| 505 | 506 | 'electronhub': SECRET_KEYS.ELECTRONHUB, |
| 506 | 507 | 'pollinations': SECRET_KEYS.POLLINATIONS, |
| 508 | + 'workers_ai': SECRET_KEYS.WORKERS_AI, | |
| 507 | 509 | }; |
| 508 | 510 | |
| 509 | 511 | if (chatCompletionApis[api] && secret_state[chatCompletionApis[api]]) { |
| @@ -580,7 +582,7 @@ jQuery(async function () { | ||
| 580 | 582 | } |
| 581 | 583 | |
| 582 | 584 | async function addRemoteEndpointModels() { |
| 583 | 585 | async function processEndpoint(api, url, additionalParams = {}) { |
| 584 | 586 | const dropdown = document.getElementById('caption_multimodal_model'); |
| 585 | 587 | if (!(dropdown instanceof HTMLSelectElement)) { |
| 586 | 588 | return; |
| @@ -591,7 +593,8 @@ jQuery(async function () { | ||
| 591 | 593 | const options = Array.from(dropdown.options); |
| 592 | 594 | const response = await fetch(url, { |
| 593 | 595 | method: 'POST', |
| 594 | 596 | headers: getRequestHeaders({ omitContentType: true }), |
| 597 | + body: JSON.stringify(additionalParams), | |
| 595 | 598 | }); |
| 596 | 599 | if (!response.ok) { |
| 597 | 600 | return; |
| @@ -620,6 +623,7 @@ jQuery(async function () { | ||
| 620 | 623 | await processEndpoint('mistral', '/api/backends/chat-completions/multimodal-models/mistral'); |
| 621 | 624 | await processEndpoint('xai', '/api/backends/chat-completions/multimodal-models/xai'); |
| 622 | 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 | 629 | await addSettings(); |
| @@ -20,6 +20,7 @@ | ||
| 20 | 20 | <option value="aimlapi">AI/ML API</option> |
| 21 | 21 | <option value="chutes">Chutes</option> |
| 22 | 22 | <option value="anthropic">Claude</option> |
| 23 | + <option value="workers_ai">Cloudflare Workers AI</option> | |
| 23 | 24 | <option value="cohere">Cohere</option> |
| 24 | 25 | <option value="custom" data-i18n="Custom (OpenAI-compatible)">Custom (OpenAI-compatible)</option> |
| 25 | 26 | <option value="electronhub">Electron Hub</option> |
| @@ -126,6 +126,10 @@ export async function getMultimodalCaption(base64Img, prompt) { | ||
| 126 | 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 | 133 | function getEndpointUrl() { |
| 130 | 134 | switch (extension_settings.caption.multimodal_api) { |
| 131 | 135 | case 'google': |
| @@ -283,6 +287,10 @@ function throwIfInvalidModel(useReverseProxy) { | ||
| 283 | 287 | if (multimodalApi === 'pollinations' && !secret_state[SECRET_KEYS.POLLINATIONS]) { |
| 284 | 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 | 389 | return 'Extras API must provide an "embeddings" module.'; |
| 390 | 390 | case 'webllm_not_supported': |
| 391 | 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 | 394 | default: |
| 393 | 395 | return 'Check server console for more details'; |
| 394 | 396 | } |
| @@ -843,6 +845,10 @@ function getVectorsRequestBody(args = {}) { | ||
| 843 | 845 | body.model = extension_settings.vectors.siliconflow_model; |
| 844 | 846 | body.siliconflow_endpoint = oai_settings.siliconflow_endpoint; |
| 845 | 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 | 852 | default: |
| 847 | 853 | break; |
| 848 | 854 | } |
| @@ -936,6 +942,7 @@ function throwIfSourceInvalid() { | ||
| 936 | 942 | settings.source === 'togetherai' && !secret_state[SECRET_KEYS.TOGETHERAI] || |
| 937 | 943 | settings.source === 'nomicai' && !secret_state[SECRET_KEYS.NOMICAI] || |
| 938 | 944 | settings.source === 'cohere' && !secret_state[SECRET_KEYS.COHERE] || |
| 945 | + settings.source === 'workers_ai' && !secret_state[SECRET_KEYS.WORKERS_AI] || | |
| 939 | 946 | settings.source === 'siliconflow' && !secret_state[SECRET_KEYS.SILICONFLOW]) { |
| 940 | 947 | throw new Error('Vectors: API key missing', { cause: 'api_key_missing' }); |
| 941 | 948 | } |
| @@ -964,6 +971,10 @@ function throwIfSourceInvalid() { | ||
| 964 | 971 | if (settings.source === 'webllm' && (!isWebLlmSupported() || !settings.webllm_model)) { |
| 965 | 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 | 1166 | $('#koboldcpp_vectorsModel').toggle(settings.source === 'koboldcpp'); |
| 1156 | 1167 | $('#google_vectorsModel').toggle(settings.source === 'palm' || settings.source === 'vertexai'); |
| 1157 | 1168 | $('#siliconflow_vectorsModel').toggle(settings.source === 'siliconflow'); |
| 1169 | + $('#workers_ai_vectorsModel').toggle(settings.source === 'workers_ai'); | |
| 1158 | 1170 | $('#vector_altEndpointUrl').toggle(vectorApiRequiresUrl.includes(settings.source)); |
| 1159 | 1171 | switch (settings.source) { |
| 1160 | 1172 | case 'webllm': |
| @@ -1175,6 +1187,9 @@ function toggleSettings() { | ||
| 1175 | 1187 | case 'siliconflow': |
| 1176 | 1188 | loadSiliconFlowModels(); |
| 1177 | 1189 | break; |
| 1190 | + case 'workers_ai': | |
| 1191 | + loadWorkersAIModels(); | |
| 1192 | + break; | |
| 1178 | 1193 | } |
| 1179 | 1194 | } |
| 1180 | 1195 | |
| @@ -1362,6 +1377,45 @@ function populateSiliconFlowModelSelect(models) { | ||
| 1362 | 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 | 1420 | * Executes a function with WebLLM error handling. |
| 1367 | 1421 | * @param {function(): Promise<T>} func Function to execute |
| @@ -1785,6 +1839,11 @@ jQuery(async () => { | ||
| 1785 | 1839 | Object.assign(extension_settings.vectors, settings); |
| 1786 | 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 | 1847 | $('#vectors_openrouter_model').val(settings.openrouter_model).on('change', () => { |
| 1789 | 1848 | settings.openrouter_model = String($('#vectors_openrouter_model').val()); |
| 1790 | 1849 | Object.assign(extension_settings.vectors, settings); |
| @@ -11,6 +11,7 @@ | ||
| 11 | 11 | </label> |
| 12 | 12 | <select id="vectors_source" class="text_pole"> |
| 13 | 13 | <option value="chutes">Chutes</option> |
| 14 | + <option value="workers_ai">Cloudflare Workers AI</option> | |
| 14 | 15 | <option value="cohere">Cohere</option> |
| 15 | 16 | <option value="electronhub">Electron Hub</option> |
| 16 | 17 | <option value="extras">Extras (deprecated)</option> |
| @@ -205,6 +206,16 @@ | ||
| 205 | 206 | </i> |
| 206 | 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 | 219 | <div class="flex-container marginTopBot5"> |
| 209 | 220 | <div class="flex-container flex1 flexFlowColumn" title="How many last messages will be matched for relevance."> |
| 210 | 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 | 2760 | router.use('/multimodal-models', multimodalModels); |
| 2728 | 2761 | |
| 2729 | 2762 | router.post('/process', async function (request, response) { |
| @@ -102,6 +102,10 @@ router.post('/caption-image', async (request, response) => { | ||
| 102 | 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 | 109 | const noKeyTypes = ['custom', 'ooba', 'koboldcpp', 'vllm', 'llamacpp']; |
| 106 | 110 | if (!key && !request.body.reverse_proxy && !noKeyTypes.includes(request.body.api)) { |
| 107 | 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 | 231 | if (['koboldcpp', 'vllm', 'llamacpp', 'ooba'].includes(request.body.api)) { |
| 220 | 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 | 629 | router.post('/generate-image', async (request, response) => { |
| 574 | 630 | try { |
| 575 | 631 | const key = readSecret(request.user.directories, SECRET_KEYS.OPENAI); |
| @@ -39,6 +39,7 @@ const SOURCES = [ | ||
| 39 | 39 | 'chutes', |
| 40 | 40 | 'nanogpt', |
| 41 | 41 | 'siliconflow', |
| 42 | + 'workers_ai', | |
| 42 | 43 | ]; |
| 43 | 44 | |
| 44 | 45 | /** |
| @@ -88,6 +89,8 @@ async function getVector(source, sourceSettings, text, isQuery, directories) { | ||
| 88 | 89 | return getOpenAIVector(text, source, directories, sourceSettings.model); |
| 89 | 90 | case 'siliconflow': |
| 90 | 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 | 96 | throw new Error(`Unknown vector source ${source}`); |
| @@ -162,6 +165,9 @@ async function getBatchVector(source, sourceSettings, texts, isQuery, directorie | ||
| 162 | 165 | case 'siliconflow': |
| 163 | 166 | results.push(...await getOpenAIBatchVector(batch, source, directories, sourceSettings.model, sourceSettings.urlOverride)); |
| 164 | 167 | break; |
| 168 | + case 'workers_ai': | |
| 169 | + results.push(...await getOpenAIBatchVector(batch, source, directories, sourceSettings.model, sourceSettings.urlOverride)); | |
| 170 | + break; | |
| 165 | 171 | default: |
| 166 | 172 | throw new Error(`Unknown vector source ${source}`); |
| 167 | 173 | } |
| @@ -260,6 +266,15 @@ function getSourceSettings(source, request) { | ||
| 260 | 266 | urlOverride: request.body.siliconflow_endpoint === 'cn' |
| 261 | 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 | 278 | default: |
| 264 | 279 | return {}; |
| 265 | 280 | } |
| @@ -61,6 +61,13 @@ const SOURCES = { | ||
| 61 | 61 | headers: {}, |
| 62 | 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 | 97 | const modelName = model || config.model; |
| 91 | 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 | 104 | const body = { |
| 93 | 105 | input: texts, |
| 94 | 106 | model: modelName, |