feat: add Cloudflare Workers AI provider (#5385) * feat: add Cloudflare Workers AI provider Adds support for Cloudflare Workers AI using its OpenAI-compatible API. Workers AI-specific stuff includes: - Model list fetching and capabilities detection - Tokenizer auto-detection for typical hosted model families - Streaming not supported when using structured output Closes #5305 * Make the entire header clickable * Add missing samplers * Fix non-streaming reasoning parsing --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>

700fc05411e8eb62c5e08a828f9c36e1e8290a4e

Tony Gies <tgies@tgies.net>

Signed
11 files changed, +210 -16Showing whitespace changes
public/index.html+32 -11
@@ -697,7 +697,7 @@
697697 </span>
698698 </div>
699699 </div>
700700 <div class="range-block" data-source="openai,claude,aimlapi,openrouter,ai21,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,siliconflow,electronhub,chutes,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai,zai,workers_ai">
701701 <div class="range-block-title" data-i18n="Temperature">
702702 Temperature
703703 </div>
@@ -710,7 +710,7 @@
710710 </div>
711711 </div>
712712 </div>
713713 <div class="range-block" data-source="openai,aimlapi,openrouter,custom,cohere,perplexity,groq,siliconflow,mistralai,electronhub,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai,chutes,workers_ai">
714714 <div class="range-block-title" data-i18n="Frequency Penalty">
715715 Frequency Penalty
716716 </div>
@@ -723,7 +723,7 @@
723723 </div>
724724 </div>
725725 </div>
726726 <div class="range-block" data-source="openai,aimlapi,openrouter,custom,cohere,perplexity,groq,siliconflow,mistralai,electronhub,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai,chutes,workers_ai">
727727 <div class="range-block-title" data-i18n="Presence Penalty">
728728 Presence Penalty
729729 </div>
@@ -736,7 +736,7 @@
736736 </div>
737737 </div>
738738 </div>
739739 <div class="range-block" data-source="claude,aimlapi,openrouter,makersuite,vertexai,cohere,perplexity,electronhub,chutes,nanogpt,workers_ai">
740740 <div class="range-block-title" data-i18n="Top K">
741741 Top K
742742 </div>
@@ -749,7 +749,7 @@
749749 </div>
750750 </div>
751751 </div>
752752 <div class="range-block" data-source="openai,claude,aimlapi,openrouter,ai21,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,siliconflow,electronhub,chutes,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai,zai,workers_ai">
753753 <div class="range-block-title" data-i18n="Top P">
754754 Top P
755755 </div>
@@ -762,7 +762,7 @@
762762 </div>
763763 </div>
764764 </div>
765765 <div class="range-block" data-source="openrouter,nanogpt,chutes,workers_ai">
766766 <div class="range-block-title" data-i18n="Repetition Penalty">
767767 Repetition Penalty
768768 </div>
@@ -986,7 +986,7 @@
986986 </div>
987987 </div>
988988 </div>
989989 <div class="range-block" data-source="openai,openrouter,mistralai,custom,cohere,groq,electronhub,chutes,nanogpt,xai,pollinations,aimlapi,makersuite,vertexai,azure_openai,workers_ai">
990990 <div class="range-block-title justifyLeft" data-i18n="Seed">
991991 Seed
992992 </div>
@@ -1996,7 +1996,7 @@
19961996 </b>
19971997 </div>
19981998 </div>
19991999 <div class="range-block" data-source="openai,cohere,mistralai,custom,claude,aimlapi,openrouter,groq,siliconflow,deepseek,makersuite,vertexai,ai21,xai,pollinations,moonshot,fireworks,cometapi,electronhub,chutes,azure_openai,zai,nanogpt,workers_ai">
20002000 <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">
20012001 <input id="openai_function_calling" type="checkbox" />
20022002 <span data-i18n="Enable function calling">Enable function calling</span>
@@ -2035,7 +2035,7 @@
20352035 </em>
20362036 </div>
20372037 </div>
20382038 <div class="range-block" data-source="openai,aimlapi,openrouter,mistralai,makersuite,vertexai,claude,custom,xai,pollinations,moonshot,cohere,cometapi,nanogpt,electronhub,azure_openai,zai,siliconflow,chutes,workers_ai">
20392039 <label for="openai_media_inlining" class="checkbox_label flexWrap widthFreeExpand">
20402040 <input id="openai_media_inlining" type="checkbox" />
20412041 <span data-i18n="Send inline media">Send inline media</span>
@@ -2059,7 +2059,7 @@
20592059 <strong data-source="makersuite,vertexai" data-i18n="video_inlining_hint_4">Videos must be less than 20 MB and under 1 minute long.</strong>
20602060 <strong data-source="makersuite,vertexai" data-i18n="audio_inlining_hint_2">Audio must be less than 20 MB.</strong>
20612061 </div>
20622062 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,xai,pollinations,cohere,cometapi,nanogpt,moonshot,aimlapi,openrouter,mistralai,electronhub,azure_openai,zai,siliconflow,chutes,makersuite,vertexai,workers_ai">
20632063 <div class="flex-container oneline-dropdown">
20642064 <label for="openai_inline_image_quality" data-i18n="Inline Image Quality">
20652065 Inline Image Quality
@@ -2120,7 +2120,7 @@
21202120 </div>
21212121 </div>
21222122 </div>
21232123 <div class="range-block" data-source="deepseek,aimlapi,openrouter,custom,claude,xai,makersuite,vertexai,pollinations,moonshot,mistralai,fireworks,cometapi,electronhub,chutes,azure_openai,nanogpt,zai,workers_ai">
21242124 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">
21252125 <input id="openai_show_thoughts" type="checkbox" />
21262126 <span data-i18n="Request model reasoning">Request model reasoning</span>
@@ -2894,6 +2894,7 @@
28942894 <option value="azure_openai">Azure OpenAI</option>
28952895 <option value="chutes">Chutes</option>
28962896 <option value="claude">Claude</option>
2897+ <option value="workers_ai">Cloudflare Workers AI</option>
28972898 <option value="cohere">Cohere</option>
28982899 <!-- Temporarily disabled. -->
28992900 <!-- <option value="cometapi">CometAPI</option> -->
@@ -3671,6 +3672,26 @@
36713672 </select>
36723673 </div>
36733674 </div>
3675+ <div id="workers_ai_form" data-source="workers_ai">
3676+ <h4><a href="https://dash.cloudflare.com/?to=/:account/ai/workers-ai/api-quick-start" target="_blank" rel="noopener noreferrer" data-i18n="Cloudflare Workers AI API Key">Cloudflare Workers AI API Key</a></h4>
3677+ <div class="flex-container">
3678+ <input id="api_key_workers_ai" name="api_key_workers_ai" class="text_pole flex1" value="" type="text" autocomplete="off">
3679+ <div title="Manage API keys" data-i18n="[title]Manage API keys" class="menu_button fa-solid fa-key fa-fw manage-api-keys" data-key="api_key_workers_ai"></div>
3680+ </div>
3681+ <div data-for="api_key_workers_ai" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you click 'Connect'.">
3682+ For privacy reasons, your API key will be hidden after you click 'Connect'.
3683+ </div>
3684+ <h4><a href="https://dash.cloudflare.com/?to=/:account/workers-and-pages" target="_blank" rel="noopener noreferrer" data-i18n="Cloudflare Account ID">Cloudflare Account ID</a></h4>
3685+ <div class="flex-container">
3686+ <input id="workers_ai_account_id" class="text_pole flex1" value="" type="text" autocomplete="off" placeholder="e.g. 023e105f4ecef8ad9ca31a8372d0c353" data-i18n="[placeholder]e.g. 023e105f4ecef8ad9ca31a8372d0c353">
3687+ </div>
3688+ <div>
3689+ <h4 data-i18n="Workers AI Model">Workers AI Model</h4>
3690+ <select id="model_workers_ai_select">
3691+ <option value="" data-i18n="-- Connect to the API --">-- Connect to the API --</option>
3692+ </select>
3693+ </div>
3694+ </div>
36743695 <div id="deepseek_form" data-source="deepseek">
36753696 <h4 data-i18n="DeepSeek API Key">DeepSeek API Key</h4>
36763697 <div class="flex-container">
public/scripts/RossAscends-mods.js+1 -0
@@ -407,6 +407,7 @@ function RA_autoconnect(PrevApi) {
407407 || (secret_state[SECRET_KEYS.COMETAPI] && oai_settings.chat_completion_source == chat_completion_sources.COMETAPI)
408408 || (secret_state[SECRET_KEYS.ZAI] && oai_settings.chat_completion_source == chat_completion_sources.ZAI)
409409 || (secret_state[SECRET_KEYS.POLLINATIONS] && oai_settings.chat_completion_source === chat_completion_sources.POLLINATIONS)
410+ || (secret_state[SECRET_KEYS.WORKERS_AI] && oai_settings.chat_completion_source == chat_completion_sources.WORKERS_AI)
410411 || (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM)
411412 || (secret_state[SECRET_KEYS.AZURE_OPENAI] && oai_settings.chat_completion_source == chat_completion_sources.AZURE_OPENAI)
412413 ) {
public/scripts/openai.js+80 -2
@@ -196,6 +196,7 @@ export const chat_completion_sources = {
196196 AZURE_OPENAI: 'azure_openai',
197197 ZAI: 'zai',
198198 SILICONFLOW: 'siliconflow',
199+ WORKERS_AI: 'workers_ai',
199200};
200201
201202const character_names_behavior = {
@@ -279,6 +280,7 @@ const sensitiveFields = [
279280 'vertexai_express_project_id',
280281 'azure_base_url',
281282 'azure_deployment_name',
283+ 'workers_ai_account_id',
282284];
283285
284286/**
@@ -337,6 +339,8 @@ export const settingsToUpdate = {
337339 vertexai_model: ['#model_vertexai_select', 'vertexai_model', false, true],
338340 zai_model: ['#model_zai_select', 'zai_model', false, true],
339341 zai_endpoint: ['#zai_endpoint', 'zai_endpoint', false, true],
342+ workers_ai_model: ['#model_workers_ai_select', 'workers_ai_model', false, true],
343+ workers_ai_account_id: ['#workers_ai_account_id', 'workers_ai_account_id', false, true],
340344 openai_max_context: ['#openai_max_context', 'openai_max_context', false, false],
341345 openai_max_tokens: ['#openai_max_tokens', 'openai_max_tokens', false, false],
342346 names_behavior: ['#names_behavior', 'names_behavior', false, false],
@@ -439,6 +443,8 @@ const default_settings = {
439443 fireworks_model: 'accounts/fireworks/models/kimi-k2-instruct',
440444 zai_model: 'glm-4.6',
441445 zai_endpoint: ZAI_ENDPOINT.COMMON,
446+ workers_ai_model: '@cf/meta/llama-3.3-70b-instruct-fp8-fast',
447+ workers_ai_account_id: '',
442448 azure_base_url: '',
443449 azure_deployment_name: '',
444450 azure_api_version: '2024-02-15-preview',
@@ -1727,6 +1733,8 @@ export function getChatCompletionModel(settings = null) {
17271733 return settings.azure_openai_model;
17281734 case chat_completion_sources.ZAI:
17291735 return settings.zai_model;
1736+ case chat_completion_sources.WORKERS_AI:
1737+ return settings.workers_ai_model;
17301738 default:
17311739 console.error(`Unknown chat completion source: ${source}`);
17321740 return '';
@@ -2193,6 +2201,24 @@ function saveModelList(data) {
21932201 $('#model_fireworks_select').val(oai_settings.fireworks_model).trigger('change');
21942202 }
21952203
2204+ if (oai_settings.chat_completion_source === chat_completion_sources.WORKERS_AI) {
2205+ $('#model_workers_ai_select').empty();
2206+ model_list.forEach((model) => {
2207+ $('#model_workers_ai_select').append(
2208+ $('<option>', {
2209+ value: model.id,
2210+ text: model.id,
2211+ }));
2212+ });
2213+
2214+ const selectedModel = model_list.find(model => model.id === oai_settings.workers_ai_model);
2215+ if (model_list.length > 0 && (!selectedModel || !oai_settings.workers_ai_model)) {
2216+ oai_settings.workers_ai_model = model_list[0].id;
2217+ }
2218+
2219+ $('#model_workers_ai_select').val(oai_settings.workers_ai_model).trigger('change');
2220+ }
2221+
21962222 if (oai_settings.chat_completion_source === chat_completion_sources.COMETAPI) {
21972223 $('#model_cometapi_select').empty();
21982224
@@ -2604,7 +2630,8 @@ export async function createGenerationParameters(settings, model, type, messages
26042630 ];
26052631
26062632 const isO1 = gptSources.includes(settings.chat_completion_source) && ['o1-2024-12-17', 'o1'].includes(model);
26072633 const streamisWorkersAIJsonMode = settings.stream_openai && typechat_completion_source !=== 'quiet'chat_completion_sources.WORKERS_AI && !isO1jsonSchema;
2634+ const stream = settings.stream_openai && type !== 'quiet' && !isO1 && !isWorkersAIJsonMode;
26082635
26092636 const noMultiSwipeTypes = ['quiet', 'impersonate', 'continue'];
26102637 const canMultiSwipe = settings.n > 1 && !noMultiSwipeTypes.includes(type) && multiswipeSources.includes(settings.chat_completion_source);
@@ -2815,6 +2842,16 @@ export async function createGenerationParameters(settings, model, type, messages
28152842 generate_data.siliconflow_endpoint = settings.siliconflow_endpoint || SILICONFLOW_ENDPOINT.GLOBAL;
28162843 }
28172844
2845+ if (settings.chat_completion_source === chat_completion_sources.WORKERS_AI) {
2846+ generate_data.workers_ai_account_id = settings.workers_ai_account_id;
2847+ generate_data.top_k = settings.top_k_openai > 0 ? Math.min(Number(settings.top_k_openai), 50) : undefined;
2848+ generate_data.repetition_penalty = Number(settings.repetition_penalty_openai);
2849+ generate_data.seed = settings.seed >= 1 ? Number(settings.seed) : undefined;
2850+ generate_data.top_p = Math.max(Number(settings.top_p_openai), 0.001);
2851+ delete generate_data.n;
2852+ delete generate_data.logit_bias;
2853+ }
2854+
28182855 // https://docs.nano-gpt.com/api-reference/endpoint/chat-completion#temperature-&-nucleus
28192856 if (settings.chat_completion_source === chat_completion_sources.NANOGPT) {
28202857 generate_data.top_k = Number(settings.top_k_openai);
@@ -3052,7 +3089,7 @@ export function getStreamingReply(data, state, { chatCompletionSource = null, ov
30523089 }
30533090 });
30543091 return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
30553092 } else if ([chat_completion_sources.CUSTOM, chat_completion_sources.POLLINATIONS, chat_completion_sources.AIMLAPI, chat_completion_sources.MOONSHOT, chat_completion_sources.COMETAPI, chat_completion_sources.ELECTRONHUB, chat_completion_sources.NANOGPT, chat_completion_sources.ZAI, chat_completion_sources.SILICONFLOW, chat_completion_sources.CHUTES, chat_completion_sources.WORKERS_AI].includes(chat_completion_source)) {
30563093 if (show_thoughts) {
30573094 state.reasoning +=
30583095 data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content ??
@@ -4272,6 +4309,10 @@ async function getStatusOpen() {
42724309 data.siliconflow_endpoint = oai_settings.siliconflow_endpoint;
42734310 }
42744311
4312+ if (oai_settings.chat_completion_source === chat_completion_sources.WORKERS_AI) {
4313+ data.workers_ai_account_id = oai_settings.workers_ai_account_id;
4314+ }
4315+
42754316 const canBypass = (oai_settings.chat_completion_source === chat_completion_sources.OPENAI && oai_settings.bypass_status_check) || oai_settings.chat_completion_source === chat_completion_sources.CUSTOM;
42764317 if (canBypass) {
42774318 setOnlineStatus(t`Status check bypassed`);
@@ -5329,6 +5370,15 @@ async function onModelChange() {
53295370 oai_settings.zai_model = value;
53305371 }
53315372
5373+ if ($(this).is('#model_workers_ai_select')) {
5374+ if (!value || !hasModelsLoaded) {
5375+ console.debug('Null Workers AI model selected. Ignoring.');
5376+ return;
5377+ }
5378+ console.log('Workers AI model changed to', value);
5379+ oai_settings.workers_ai_model = value;
5380+ }
5381+
53325382 if ([chat_completion_sources.MAKERSUITE, chat_completion_sources.VERTEXAI].includes(oai_settings.chat_completion_source)) {
53335383 if (oai_settings.max_context_unlocked) {
53345384 $('#openai_max_context').attr('max', max_2mil);
@@ -5549,6 +5599,22 @@ async function onModelChange() {
55495599 $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
55505600 }
55515601
5602+ if (oai_settings.chat_completion_source === chat_completion_sources.WORKERS_AI) {
5603+ if (oai_settings.max_context_unlocked) {
5604+ $('#openai_max_context').attr('max', unlocked_max);
5605+ } else {
5606+ const model = model_list.find(m => m.id === oai_settings.workers_ai_model);
5607+ const ctxProp = Array.isArray(model?.properties) && model.properties.find(p => p.property_id === 'context_window');
5608+ const contextLength = ctxProp ? Number(ctxProp.value) : max_8k;
5609+ $('#openai_max_context').attr('max', contextLength || max_8k);
5610+ }
5611+ oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
5612+ $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
5613+ const workersAiMaxTemp = 5.0;
5614+ oai_settings.temp_openai = Math.min(workersAiMaxTemp, oai_settings.temp_openai);
5615+ $('#temp_openai').attr('max', workersAiMaxTemp).val(oai_settings.temp_openai).trigger('input');
5616+ }
5617+
55525618 if (oai_settings.chat_completion_source === chat_completion_sources.COMETAPI) {
55535619 $('#openai_max_context').attr('max', oai_settings.max_context_unlocked ? unlocked_max : max_128k);
55545620 oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
@@ -5710,6 +5776,7 @@ async function onConnectButtonClick(e) {
57105776 [chat_completion_sources.ZAI]: { key: SECRET_KEYS.ZAI, selector: '#api_key_zai', proxy: true },
57115777 [chat_completion_sources.CHUTES]: { key: SECRET_KEYS.CHUTES, selector: '#api_key_chutes', proxy: false },
57125778 [chat_completion_sources.POLLINATIONS]: { key: SECRET_KEYS.POLLINATIONS, selector: '#api_key_pollinations', proxy: false },
5779+ [chat_completion_sources.WORKERS_AI]: { key: SECRET_KEYS.WORKERS_AI, selector: '#api_key_workers_ai', proxy: false },
57135780 };
57145781
57155782 // Vertex AI Express version - use API key
@@ -5799,6 +5866,8 @@ function toggleChatCompletionForms() {
57995866 $('#azure_openai_model').trigger('change');
58005867 } else if (oai_settings.chat_completion_source == chat_completion_sources.ZAI) {
58015868 $('#model_zai_select').trigger('change');
5869+ } else if (oai_settings.chat_completion_source == chat_completion_sources.WORKERS_AI) {
5870+ $('#model_workers_ai_select').trigger('change');
58025871 }
58035872
58045873 $('[data-source]').each(function () {
@@ -5980,6 +6049,10 @@ export function isImageInliningSupported() {
59806049 return visionSupportedModels.some(model => oai_settings.zai_model.includes(model));
59816050 case chat_completion_sources.SILICONFLOW:
59826051 return visionSupportedModels.some(model => oai_settings.siliconflow_model.includes(model));
6052+ case chat_completion_sources.WORKERS_AI: {
6053+ const waiModel = Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.workers_ai_model);
6054+ return Boolean(waiModel && Array.isArray(waiModel.properties) && waiModel.properties.some(p => p.property_id === 'vision' && p.value === 'true'));
6055+ }
59836056 default:
59846057 return false;
59856058 }
@@ -6952,6 +7025,10 @@ export function initOpenAI() {
69527025 oai_settings.siliconflow_endpoint = String($(this).val());
69537026 saveSettingsDebounced();
69547027 });
7028+ $('#workers_ai_account_id').on('input', function () {
7029+ oai_settings.workers_ai_account_id = String($(this).val());
7030+ saveSettingsDebounced();
7031+ });
69557032 $('#vertexai_service_account_json').on('input', onVertexAIServiceAccountJsonChange);
69567033 $('#vertexai_validate_service_account').on('click', onVertexAIValidateServiceAccount);
69577034 $('#vertexai_clear_service_account').on('click', onVertexAIClearServiceAccount);
@@ -6980,6 +7057,7 @@ export function initOpenAI() {
69807057 $('#model_fireworks_select').on('change', onModelChange);
69817058 $('#azure_openai_model').on('change', onModelChange);
69827059 $('#model_zai_select').on('change', onModelChange);
7060+ $('#model_workers_ai_select').on('change', onModelChange);
69837061 $('#settings_preset_openai').on('change', onSettingsPresetChange);
69847062 $('#new_oai_preset').on('click', onNewPresetClick);
69857063 $('#delete_oai_preset').on('click', onDeletePresetClick);
public/scripts/reasoning.js+4 -3
@@ -134,6 +134,7 @@ export function extractReasoningFromData(data, {
134134 case chat_completion_sources.NANOGPT:
135135 case chat_completion_sources.SILICONFLOW:
136136 case chat_completion_sources.ZAI:
137+ case chat_completion_sources.WORKERS_AI:
137138 case chat_completion_sources.CUSTOM: {
138139 return data?.choices?.[0]?.message?.reasoning_content
139140 ?? data?.choices?.[0]?.message?.reasoning
@@ -1082,7 +1083,7 @@ function registerReasoningSlashCommands() {
10821083 helpString: t`Collapse the reasoning block of a message or range of messages.`,
10831084 unnamedArgumentList: reasoningVisibilityArgs,
10841085 callback: (_args, value) => {
10851086 const details = getReasoningDetailsElements(value.toString());
10861087 if (details) details.removeAttr('open');
10871088 return '';
10881089 },
@@ -1094,7 +1095,7 @@ function registerReasoningSlashCommands() {
10941095 helpString: t`Expand the reasoning block of a message or range of messages.`,
10951096 unnamedArgumentList: reasoningVisibilityArgs,
10961097 callback: (_args, value) => {
10971098 const details = getReasoningDetailsElements(value.toString());
10981099 if (details) details.attr('open', '');
10991100 return '';
11001101 },
@@ -1106,7 +1107,7 @@ function registerReasoningSlashCommands() {
11061107 helpString: t`Toggle the reasoning block of a message or range of messages. Expanded blocks will be collapsed, and collapsed blocks will be expanded.`,
11071108 unnamedArgumentList: reasoningVisibilityArgs,
11081109 callback: (_args, value) => {
11091110 const details = getReasoningDetailsElements(value.toString());
11101111 if (!details) return '';
11111112 details.each(function () {
11121113 const $el = $(this);
public/scripts/secrets.js+3 -0
@@ -76,6 +76,7 @@ export const SECRET_KEYS = {
7676 POLLINATIONS: 'api_key_pollinations',
7777 VOLCENGINE_APP_ID: 'volcengine_app_id',
7878 VOLCENGINE_ACCESS_KEY: 'volcengine_access_key',
79+ WORKERS_AI: 'api_key_workers_ai',
7980};
8081
8182const FRIENDLY_NAMES = {
@@ -140,6 +141,7 @@ const FRIENDLY_NAMES = {
140141 [SECRET_KEYS.POLLINATIONS]: 'Pollinations',
141142 [SECRET_KEYS.VOLCENGINE_APP_ID]: 'Volcengine App ID',
142143 [SECRET_KEYS.VOLCENGINE_ACCESS_KEY]: 'Volcengine Access Key',
144+ [SECRET_KEYS.WORKERS_AI]: 'Cloudflare Workers AI',
143145};
144146
145147const INPUT_MAP = {
@@ -184,6 +186,7 @@ const INPUT_MAP = {
184186 [SECRET_KEYS.SILICONFLOW]: '#api_key_siliconflow',
185187 [SECRET_KEYS.COMFY_RUNPOD]: '#api_key_comfy_runpod',
186188 [SECRET_KEYS.POLLINATIONS]: '#api_key_pollinations',
189+ [SECRET_KEYS.WORKERS_AI]: '#api_key_workers_ai',
187190};
188191
189192const getLabel = () => moment().format('L LT');
public/scripts/slash-commands.js+1 -0
@@ -6386,6 +6386,7 @@ function getModelOptions(quiet) {
63866386 { id: 'model_fireworks_select', api: 'openai', type: chat_completion_sources.FIREWORKS },
63876387 { id: 'model_cometapi_select', api: 'openai', type: chat_completion_sources.COMETAPI },
63886388 { id: 'model_zai_select', api: 'openai', type: chat_completion_sources.ZAI },
6389+ { id: 'model_workers_ai_select', api: 'openai', type: chat_completion_sources.WORKERS_AI },
63896390 { id: 'model_novel_select', api: 'novel', type: null },
63906391 { id: 'horde_model', api: 'koboldhorde', type: null },
63916392 ];
public/scripts/tokenizers.js+22 -0
@@ -694,6 +694,28 @@ export function getTokenizerModel() {
694694 }
695695 }
696696
697+ if (oai_settings.chat_completion_source == chat_completion_sources.WORKERS_AI && oai_settings.workers_ai_model) {
698+ const model = oai_settings.workers_ai_model.toLowerCase();
699+
700+ if (model.includes('deepseek')) {
701+ return deepseekTokenizer;
702+ } else if (model.includes('qwen') || model.includes('qwq') || model.includes('kimi')) {
703+ return qwen2Tokenizer;
704+ } else if (model.includes('llama-3') || model.includes('llama-4')) {
705+ return llama3Tokenizer;
706+ } else if (model.includes('llama')) {
707+ return llamaTokenizer;
708+ } else if (model.includes('gemma')) {
709+ return gemmaTokenizer;
710+ } else if (model.includes('mistral')) {
711+ return mistralTokenizer;
712+ } else if (model.includes('phi')) {
713+ return turboTokenizer;
714+ } else if (model.includes('gpt-oss')) {
715+ return gpt4oTokenizer;
716+ }
717+ }
718+
697719 if (oai_settings.chat_completion_source == chat_completion_sources.COHERE) {
698720 if (oai_settings.cohere_model.includes('command-a')) {
699721 return commandATokenizer;
public/scripts/tool-calling.js+3 -0
@@ -637,6 +637,8 @@ export class ToolManager {
637637 return currentModel.supported_features?.includes('tools');
638638 case chat_completion_sources.ELECTRONHUB:
639639 return currentModel.metadata?.function_call;
640+ case chat_completion_sources.WORKERS_AI:
641+ return Array.isArray(currentModel.properties) && currentModel.properties.some(p => p.property_id === 'function_calling' && p.value === 'true');
640642 }
641643 }
642644
@@ -664,6 +666,7 @@ export class ToolManager {
664666 chat_completion_sources.ZAI,
665667 chat_completion_sources.SILICONFLOW,
666668 chat_completion_sources.NANOGPT,
669+ chat_completion_sources.WORKERS_AI,
667670 ];
668671 return supportedSources.includes(settings.chat_completion_source);
669672 }
src/constants.js+1 -0
@@ -209,6 +209,7 @@ export const CHAT_COMPLETION_SOURCES = {
209209 AZURE_OPENAI: 'azure_openai',
210210 ZAI: 'zai',
211211 SILICONFLOW: 'siliconflow',
212+ WORKERS_AI: 'workers_ai',
212213};
213214
214215/**
src/endpoints/backends/chat-completions.js+62 -0
@@ -90,6 +90,7 @@ const API_ZAI_CODING = 'https://api.z.ai/api/coding/paas/v4';
9090const API_SILICONFLOW = 'https://api.siliconflow.com/v1';
9191const API_SILICONFLOW_CN = 'https://api.siliconflow.cn/v1';
9292const API_OPENROUTER = 'https://openrouter.ai/api/v1';
93+const API_WORKERS_AI = 'https://api.cloudflare.com/client/v4/accounts';
9394
9495/**
9596 * Module-scoped Claude caching configuration values.
@@ -1833,6 +1834,49 @@ router.post('/status', async function (request, statusResponse) {
18331834 apiKey = readSecret(request.user.directories, SECRET_KEYS.SILICONFLOW, request.body.secret_id);
18341835 headers = {};
18351836 queryParams = { type: 'text', sub_type: 'chat' };
1837+ } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.WORKERS_AI) {
1838+ apiKey = readSecret(request.user.directories, SECRET_KEYS.WORKERS_AI, request.body.secret_id);
1839+
1840+ if (!apiKey) {
1841+ console.warn('Cloudflare Workers AI API key is missing.');
1842+ return statusResponse.status(400).send({ error: true });
1843+ }
1844+
1845+ try {
1846+ const accountId = String(request.body.workers_ai_account_id || '').trim();
1847+ if (!accountId) {
1848+ console.warn('Cloudflare Workers AI Account ID is missing.');
1849+ return statusResponse.status(400).send({ error: true });
1850+ }
1851+
1852+ const modelsUrl = new URL(`${API_WORKERS_AI}/${encodeURIComponent(accountId)}/ai/models/search`);
1853+ modelsUrl.searchParams.set('task', 'Text Generation');
1854+ modelsUrl.searchParams.set('per_page', '1000');
1855+
1856+ const response = await fetch(modelsUrl, {
1857+ method: 'GET',
1858+ headers: {
1859+ 'Authorization': 'Bearer ' + apiKey,
1860+ },
1861+ });
1862+
1863+ if (response.ok) {
1864+ /** @type {any} */
1865+ const data = await response.json();
1866+ const models = Array.isArray(data?.result)
1867+ ? data.result.map(model => ({ ...model, id: model.name }))
1868+ : [];
1869+
1870+ console.debug('Available Cloudflare Workers AI models:', models.map(m => m.id));
1871+ return statusResponse.send({ data: models });
1872+ } else {
1873+ console.warn('Cloudflare Workers AI models endpoint failed:', response.status, response.statusText);
1874+ return statusResponse.status(response.status).send({ error: true });
1875+ }
1876+ } catch (error) {
1877+ console.error('Error fetching Cloudflare Workers AI models:', error);
1878+ return statusResponse.status(500).send({ error: true });
1879+ }
18361880 } else {
18371881 console.warn('This chat completion source is not supported yet.');
18381882 return statusResponse.status(400).send({ error: true });
@@ -2315,6 +2359,24 @@ router.post('/generate', async function (request, response) {
23152359 if (request.body.json_schema) {
23162360 setJsonObjectFormat(bodyParams, request.body.messages, request.body.json_schema);
23172361 }
2362+ } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.WORKERS_AI) {
2363+ apiKey = readSecret(request.user.directories, SECRET_KEYS.WORKERS_AI, request.body.secret_id);
2364+ const accountId = String(request.body.workers_ai_account_id || '').trim();
2365+ if (!accountId) {
2366+ console.warn('Cloudflare Workers AI Account ID is missing.');
2367+ return response.status(400).send({ error: true });
2368+ }
2369+ apiUrl = `${API_WORKERS_AI}/${encodeURIComponent(accountId)}/ai/v1`;
2370+ headers = {};
2371+ bodyParams = {
2372+ repetition_penalty: request.body.repetition_penalty,
2373+ };
2374+ if (request.body.json_schema) {
2375+ bodyParams['response_format'] = {
2376+ type: 'json_schema',
2377+ json_schema: request.body.json_schema.value,
2378+ };
2379+ }
23182380 } else {
23192381 console.warn('This chat completion source is not supported yet.');
23202382 return response.status(400).send({ error: true });
src/endpoints/secrets.js+1 -0
@@ -69,6 +69,7 @@ export const SECRET_KEYS = {
6969 POLLINATIONS: 'api_key_pollinations',
7070 VOLCENGINE_APP_ID: 'volcengine_app_id',
7171 VOLCENGINE_ACCESS_KEY: 'volcengine_access_key',
72+ WORKERS_AI: 'api_key_workers_ai',
7273};
7374
7475/**