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 @@
697 </span>697 </span>
698 </div>698 </div>
699 </div>699 </div>
700 <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">700 <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">
701 <div class="range-block-title" data-i18n="Temperature">701 <div class="range-block-title" data-i18n="Temperature">
702 Temperature702 Temperature
703 </div>703 </div>
@@ -710,7 +710,7 @@
710 </div>710 </div>
711 </div>711 </div>
712 </div>712 </div>
713 <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">713 <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">
714 <div class="range-block-title" data-i18n="Frequency Penalty">714 <div class="range-block-title" data-i18n="Frequency Penalty">
715 Frequency Penalty715 Frequency Penalty
716 </div>716 </div>
@@ -723,7 +723,7 @@
723 </div>723 </div>
724 </div>724 </div>
725 </div>725 </div>
726 <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">726 <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">
727 <div class="range-block-title" data-i18n="Presence Penalty">727 <div class="range-block-title" data-i18n="Presence Penalty">
728 Presence Penalty728 Presence Penalty
729 </div>729 </div>
@@ -736,7 +736,7 @@
736 </div>736 </div>
737 </div>737 </div>
738 </div>738 </div>
739 <div class="range-block" data-source="claude,aimlapi,openrouter,makersuite,vertexai,cohere,perplexity,electronhub,chutes,nanogpt">739 <div class="range-block" data-source="claude,aimlapi,openrouter,makersuite,vertexai,cohere,perplexity,electronhub,chutes,nanogpt,workers_ai">
740 <div class="range-block-title" data-i18n="Top K">740 <div class="range-block-title" data-i18n="Top K">
741 Top K741 Top K
742 </div>742 </div>
@@ -749,7 +749,7 @@
749 </div>749 </div>
750 </div>750 </div>
751 </div>751 </div>
752 <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">752 <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">
753 <div class="range-block-title" data-i18n="Top P">753 <div class="range-block-title" data-i18n="Top P">
754 Top P754 Top P
755 </div>755 </div>
@@ -762,7 +762,7 @@
762 </div>762 </div>
763 </div>763 </div>
764 </div>764 </div>
765 <div class="range-block" data-source="openrouter,nanogpt,chutes">765 <div class="range-block" data-source="openrouter,nanogpt,chutes,workers_ai">
766 <div class="range-block-title" data-i18n="Repetition Penalty">766 <div class="range-block-title" data-i18n="Repetition Penalty">
767 Repetition Penalty767 Repetition Penalty
768 </div>768 </div>
@@ -986,7 +986,7 @@
986 </div>986 </div>
987 </div>987 </div>
988 </div>988 </div>
989 <div class="range-block" data-source="openai,openrouter,mistralai,custom,cohere,groq,electronhub,chutes,nanogpt,xai,pollinations,aimlapi,makersuite,vertexai,azure_openai">989 <div class="range-block" data-source="openai,openrouter,mistralai,custom,cohere,groq,electronhub,chutes,nanogpt,xai,pollinations,aimlapi,makersuite,vertexai,azure_openai,workers_ai">
990 <div class="range-block-title justifyLeft" data-i18n="Seed">990 <div class="range-block-title justifyLeft" data-i18n="Seed">
991 Seed991 Seed
992 </div>992 </div>
@@ -1996,7 +1996,7 @@
1996 </b>1996 </b>
1997 </div>1997 </div>
1998 </div>1998 </div>
1999 <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">1999 <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">
2000 <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">2000 <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">
2001 <input id="openai_function_calling" type="checkbox" />2001 <input id="openai_function_calling" type="checkbox" />
2002 <span data-i18n="Enable function calling">Enable function calling</span>2002 <span data-i18n="Enable function calling">Enable function calling</span>
@@ -2035,7 +2035,7 @@
2035 </em>2035 </em>
2036 </div>2036 </div>
2037 </div>2037 </div>
2038 <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">2038 <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">
2039 <label for="openai_media_inlining" class="checkbox_label flexWrap widthFreeExpand">2039 <label for="openai_media_inlining" class="checkbox_label flexWrap widthFreeExpand">
2040 <input id="openai_media_inlining" type="checkbox" />2040 <input id="openai_media_inlining" type="checkbox" />
2041 <span data-i18n="Send inline media">Send inline media</span>2041 <span data-i18n="Send inline media">Send inline media</span>
@@ -2059,7 +2059,7 @@
2059 <strong data-source="makersuite,vertexai" data-i18n="video_inlining_hint_4">Videos must be less than 20 MB and under 1 minute long.</strong>2059 <strong data-source="makersuite,vertexai" data-i18n="video_inlining_hint_4">Videos must be less than 20 MB and under 1 minute long.</strong>
2060 <strong data-source="makersuite,vertexai" data-i18n="audio_inlining_hint_2">Audio must be less than 20 MB.</strong>2060 <strong data-source="makersuite,vertexai" data-i18n="audio_inlining_hint_2">Audio must be less than 20 MB.</strong>
2061 </div>2061 </div>
2062 <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">2062 <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">
2063 <div class="flex-container oneline-dropdown">2063 <div class="flex-container oneline-dropdown">
2064 <label for="openai_inline_image_quality" data-i18n="Inline Image Quality">2064 <label for="openai_inline_image_quality" data-i18n="Inline Image Quality">
2065 Inline Image Quality2065 Inline Image Quality
@@ -2120,7 +2120,7 @@
2120 </div>2120 </div>
2121 </div>2121 </div>
2122 </div>2122 </div>
2123 <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">2123 <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">
2124 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">2124 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">
2125 <input id="openai_show_thoughts" type="checkbox" />2125 <input id="openai_show_thoughts" type="checkbox" />
2126 <span data-i18n="Request model reasoning">Request model reasoning</span>2126 <span data-i18n="Request model reasoning">Request model reasoning</span>
@@ -2894,6 +2894,7 @@
2894 <option value="azure_openai">Azure OpenAI</option>2894 <option value="azure_openai">Azure OpenAI</option>
2895 <option value="chutes">Chutes</option>2895 <option value="chutes">Chutes</option>
2896 <option value="claude">Claude</option>2896 <option value="claude">Claude</option>
2897 <option value="workers_ai">Cloudflare Workers AI</option>
2897 <option value="cohere">Cohere</option>2898 <option value="cohere">Cohere</option>
2898 <!-- Temporarily disabled. -->2899 <!-- Temporarily disabled. -->
2899 <!-- <option value="cometapi">CometAPI</option> -->2900 <!-- <option value="cometapi">CometAPI</option> -->
@@ -3671,6 +3672,26 @@
3671 </select>3672 </select>
3672 </div>3673 </div>
3673 </div>3674 </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>
3674 <div id="deepseek_form" data-source="deepseek">3695 <div id="deepseek_form" data-source="deepseek">
3675 <h4 data-i18n="DeepSeek API Key">DeepSeek API Key</h4>3696 <h4 data-i18n="DeepSeek API Key">DeepSeek API Key</h4>
3676 <div class="flex-container">3697 <div class="flex-container">
public/scripts/RossAscends-mods.js+1 -0
@@ -407,6 +407,7 @@ function RA_autoconnect(PrevApi) {
407 || (secret_state[SECRET_KEYS.COMETAPI] && oai_settings.chat_completion_source == chat_completion_sources.COMETAPI)407 || (secret_state[SECRET_KEYS.COMETAPI] && oai_settings.chat_completion_source == chat_completion_sources.COMETAPI)
408 || (secret_state[SECRET_KEYS.ZAI] && oai_settings.chat_completion_source == chat_completion_sources.ZAI)408 || (secret_state[SECRET_KEYS.ZAI] && oai_settings.chat_completion_source == chat_completion_sources.ZAI)
409 || (secret_state[SECRET_KEYS.POLLINATIONS] && oai_settings.chat_completion_source === chat_completion_sources.POLLINATIONS)409 || (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)
410 || (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM)411 || (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM)
411 || (secret_state[SECRET_KEYS.AZURE_OPENAI] && oai_settings.chat_completion_source == chat_completion_sources.AZURE_OPENAI)412 || (secret_state[SECRET_KEYS.AZURE_OPENAI] && oai_settings.chat_completion_source == chat_completion_sources.AZURE_OPENAI)
412 ) {413 ) {
public/scripts/openai.js+80 -2
@@ -196,6 +196,7 @@ export const chat_completion_sources = {
196 AZURE_OPENAI: 'azure_openai',196 AZURE_OPENAI: 'azure_openai',
197 ZAI: 'zai',197 ZAI: 'zai',
198 SILICONFLOW: 'siliconflow',198 SILICONFLOW: 'siliconflow',
199 WORKERS_AI: 'workers_ai',
199};200};
200201
201const character_names_behavior = {202const character_names_behavior = {
@@ -279,6 +280,7 @@ const sensitiveFields = [
279 'vertexai_express_project_id',280 'vertexai_express_project_id',
280 'azure_base_url',281 'azure_base_url',
281 'azure_deployment_name',282 'azure_deployment_name',
283 'workers_ai_account_id',
282];284];
283285
284/**286/**
@@ -337,6 +339,8 @@ export const settingsToUpdate = {
337 vertexai_model: ['#model_vertexai_select', 'vertexai_model', false, true],339 vertexai_model: ['#model_vertexai_select', 'vertexai_model', false, true],
338 zai_model: ['#model_zai_select', 'zai_model', false, true],340 zai_model: ['#model_zai_select', 'zai_model', false, true],
339 zai_endpoint: ['#zai_endpoint', 'zai_endpoint', false, true],341 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],
340 openai_max_context: ['#openai_max_context', 'openai_max_context', false, false],344 openai_max_context: ['#openai_max_context', 'openai_max_context', false, false],
341 openai_max_tokens: ['#openai_max_tokens', 'openai_max_tokens', false, false],345 openai_max_tokens: ['#openai_max_tokens', 'openai_max_tokens', false, false],
342 names_behavior: ['#names_behavior', 'names_behavior', false, false],346 names_behavior: ['#names_behavior', 'names_behavior', false, false],
@@ -439,6 +443,8 @@ const default_settings = {
439 fireworks_model: 'accounts/fireworks/models/kimi-k2-instruct',443 fireworks_model: 'accounts/fireworks/models/kimi-k2-instruct',
440 zai_model: 'glm-4.6',444 zai_model: 'glm-4.6',
441 zai_endpoint: ZAI_ENDPOINT.COMMON,445 zai_endpoint: ZAI_ENDPOINT.COMMON,
446 workers_ai_model: '@cf/meta/llama-3.3-70b-instruct-fp8-fast',
447 workers_ai_account_id: '',
442 azure_base_url: '',448 azure_base_url: '',
443 azure_deployment_name: '',449 azure_deployment_name: '',
444 azure_api_version: '2024-02-15-preview',450 azure_api_version: '2024-02-15-preview',
@@ -1727,6 +1733,8 @@ export function getChatCompletionModel(settings = null) {
1727 return settings.azure_openai_model;1733 return settings.azure_openai_model;
1728 case chat_completion_sources.ZAI:1734 case chat_completion_sources.ZAI:
1729 return settings.zai_model;1735 return settings.zai_model;
1736 case chat_completion_sources.WORKERS_AI:
1737 return settings.workers_ai_model;
1730 default:1738 default:
1731 console.error(`Unknown chat completion source: ${source}`);1739 console.error(`Unknown chat completion source: ${source}`);
1732 return '';1740 return '';
@@ -2193,6 +2201,24 @@ function saveModelList(data) {
2193 $('#model_fireworks_select').val(oai_settings.fireworks_model).trigger('change');2201 $('#model_fireworks_select').val(oai_settings.fireworks_model).trigger('change');
2194 }2202 }
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
2196 if (oai_settings.chat_completion_source === chat_completion_sources.COMETAPI) {2222 if (oai_settings.chat_completion_source === chat_completion_sources.COMETAPI) {
2197 $('#model_cometapi_select').empty();2223 $('#model_cometapi_select').empty();
21982224
@@ -2604,7 +2630,8 @@ export async function createGenerationParameters(settings, model, type, messages
2604 ];2630 ];
26052631
2606 const isO1 = gptSources.includes(settings.chat_completion_source) && ['o1-2024-12-17', 'o1'].includes(model);2632 const isO1 = gptSources.includes(settings.chat_completion_source) && ['o1-2024-12-17', 'o1'].includes(model);
2607 const stream = settings.stream_openai && type !== 'quiet' && !isO1;2633 const isWorkersAIJsonMode = settings.chat_completion_source === chat_completion_sources.WORKERS_AI && jsonSchema;
2634 const stream = settings.stream_openai && type !== 'quiet' && !isO1 && !isWorkersAIJsonMode;
26082635
2609 const noMultiSwipeTypes = ['quiet', 'impersonate', 'continue'];2636 const noMultiSwipeTypes = ['quiet', 'impersonate', 'continue'];
2610 const canMultiSwipe = settings.n > 1 && !noMultiSwipeTypes.includes(type) && multiswipeSources.includes(settings.chat_completion_source);2637 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
2815 generate_data.siliconflow_endpoint = settings.siliconflow_endpoint || SILICONFLOW_ENDPOINT.GLOBAL;2842 generate_data.siliconflow_endpoint = settings.siliconflow_endpoint || SILICONFLOW_ENDPOINT.GLOBAL;
2816 }2843 }
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
2818 // https://docs.nano-gpt.com/api-reference/endpoint/chat-completion#temperature-&-nucleus2855 // https://docs.nano-gpt.com/api-reference/endpoint/chat-completion#temperature-&-nucleus
2819 if (settings.chat_completion_source === chat_completion_sources.NANOGPT) {2856 if (settings.chat_completion_source === chat_completion_sources.NANOGPT) {
2820 generate_data.top_k = Number(settings.top_k_openai);2857 generate_data.top_k = Number(settings.top_k_openai);
@@ -3052,7 +3089,7 @@ export function getStreamingReply(data, state, { chatCompletionSource = null, ov
3052 }3089 }
3053 });3090 });
3054 return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';3091 return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
3055 } 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].includes(chat_completion_source)) {3092 } 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)) {
3056 if (show_thoughts) {3093 if (show_thoughts) {
3057 state.reasoning +=3094 state.reasoning +=
3058 data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content ??3095 data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content ??
@@ -4272,6 +4309,10 @@ async function getStatusOpen() {
4272 data.siliconflow_endpoint = oai_settings.siliconflow_endpoint;4309 data.siliconflow_endpoint = oai_settings.siliconflow_endpoint;
4273 }4310 }
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
4275 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;4316 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;
4276 if (canBypass) {4317 if (canBypass) {
4277 setOnlineStatus(t`Status check bypassed`);4318 setOnlineStatus(t`Status check bypassed`);
@@ -5329,6 +5370,15 @@ async function onModelChange() {
5329 oai_settings.zai_model = value;5370 oai_settings.zai_model = value;
5330 }5371 }
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
5332 if ([chat_completion_sources.MAKERSUITE, chat_completion_sources.VERTEXAI].includes(oai_settings.chat_completion_source)) {5382 if ([chat_completion_sources.MAKERSUITE, chat_completion_sources.VERTEXAI].includes(oai_settings.chat_completion_source)) {
5333 if (oai_settings.max_context_unlocked) {5383 if (oai_settings.max_context_unlocked) {
5334 $('#openai_max_context').attr('max', max_2mil);5384 $('#openai_max_context').attr('max', max_2mil);
@@ -5549,6 +5599,22 @@ async function onModelChange() {
5549 $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');5599 $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
5550 }5600 }
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
5552 if (oai_settings.chat_completion_source === chat_completion_sources.COMETAPI) {5618 if (oai_settings.chat_completion_source === chat_completion_sources.COMETAPI) {
5553 $('#openai_max_context').attr('max', oai_settings.max_context_unlocked ? unlocked_max : max_128k);5619 $('#openai_max_context').attr('max', oai_settings.max_context_unlocked ? unlocked_max : max_128k);
5554 oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);5620 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) {
5710 [chat_completion_sources.ZAI]: { key: SECRET_KEYS.ZAI, selector: '#api_key_zai', proxy: true },5776 [chat_completion_sources.ZAI]: { key: SECRET_KEYS.ZAI, selector: '#api_key_zai', proxy: true },
5711 [chat_completion_sources.CHUTES]: { key: SECRET_KEYS.CHUTES, selector: '#api_key_chutes', proxy: false },5777 [chat_completion_sources.CHUTES]: { key: SECRET_KEYS.CHUTES, selector: '#api_key_chutes', proxy: false },
5712 [chat_completion_sources.POLLINATIONS]: { key: SECRET_KEYS.POLLINATIONS, selector: '#api_key_pollinations', proxy: false },5778 [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 },
5713 };5780 };
57145781
5715 // Vertex AI Express version - use API key5782 // Vertex AI Express version - use API key
@@ -5799,6 +5866,8 @@ function toggleChatCompletionForms() {
5799 $('#azure_openai_model').trigger('change');5866 $('#azure_openai_model').trigger('change');
5800 } else if (oai_settings.chat_completion_source == chat_completion_sources.ZAI) {5867 } else if (oai_settings.chat_completion_source == chat_completion_sources.ZAI) {
5801 $('#model_zai_select').trigger('change');5868 $('#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');
5802 }5871 }
58035872
5804 $('[data-source]').each(function () {5873 $('[data-source]').each(function () {
@@ -5980,6 +6049,10 @@ export function isImageInliningSupported() {
5980 return visionSupportedModels.some(model => oai_settings.zai_model.includes(model));6049 return visionSupportedModels.some(model => oai_settings.zai_model.includes(model));
5981 case chat_completion_sources.SILICONFLOW:6050 case chat_completion_sources.SILICONFLOW:
5982 return visionSupportedModels.some(model => oai_settings.siliconflow_model.includes(model));6051 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 }
5983 default:6056 default:
5984 return false;6057 return false;
5985 }6058 }
@@ -6952,6 +7025,10 @@ export function initOpenAI() {
6952 oai_settings.siliconflow_endpoint = String($(this).val());7025 oai_settings.siliconflow_endpoint = String($(this).val());
6953 saveSettingsDebounced();7026 saveSettingsDebounced();
6954 });7027 });
7028 $('#workers_ai_account_id').on('input', function () {
7029 oai_settings.workers_ai_account_id = String($(this).val());
7030 saveSettingsDebounced();
7031 });
6955 $('#vertexai_service_account_json').on('input', onVertexAIServiceAccountJsonChange);7032 $('#vertexai_service_account_json').on('input', onVertexAIServiceAccountJsonChange);
6956 $('#vertexai_validate_service_account').on('click', onVertexAIValidateServiceAccount);7033 $('#vertexai_validate_service_account').on('click', onVertexAIValidateServiceAccount);
6957 $('#vertexai_clear_service_account').on('click', onVertexAIClearServiceAccount);7034 $('#vertexai_clear_service_account').on('click', onVertexAIClearServiceAccount);
@@ -6980,6 +7057,7 @@ export function initOpenAI() {
6980 $('#model_fireworks_select').on('change', onModelChange);7057 $('#model_fireworks_select').on('change', onModelChange);
6981 $('#azure_openai_model').on('change', onModelChange);7058 $('#azure_openai_model').on('change', onModelChange);
6982 $('#model_zai_select').on('change', onModelChange);7059 $('#model_zai_select').on('change', onModelChange);
7060 $('#model_workers_ai_select').on('change', onModelChange);
6983 $('#settings_preset_openai').on('change', onSettingsPresetChange);7061 $('#settings_preset_openai').on('change', onSettingsPresetChange);
6984 $('#new_oai_preset').on('click', onNewPresetClick);7062 $('#new_oai_preset').on('click', onNewPresetClick);
6985 $('#delete_oai_preset').on('click', onDeletePresetClick);7063 $('#delete_oai_preset').on('click', onDeletePresetClick);
public/scripts/reasoning.js+4 -3
@@ -134,6 +134,7 @@ export function extractReasoningFromData(data, {
134 case chat_completion_sources.NANOGPT:134 case chat_completion_sources.NANOGPT:
135 case chat_completion_sources.SILICONFLOW:135 case chat_completion_sources.SILICONFLOW:
136 case chat_completion_sources.ZAI:136 case chat_completion_sources.ZAI:
137 case chat_completion_sources.WORKERS_AI:
137 case chat_completion_sources.CUSTOM: {138 case chat_completion_sources.CUSTOM: {
138 return data?.choices?.[0]?.message?.reasoning_content139 return data?.choices?.[0]?.message?.reasoning_content
139 ?? data?.choices?.[0]?.message?.reasoning140 ?? data?.choices?.[0]?.message?.reasoning
@@ -1082,7 +1083,7 @@ function registerReasoningSlashCommands() {
1082 helpString: t`Collapse the reasoning block of a message or range of messages.`,1083 helpString: t`Collapse the reasoning block of a message or range of messages.`,
1083 unnamedArgumentList: reasoningVisibilityArgs,1084 unnamedArgumentList: reasoningVisibilityArgs,
1084 callback: (_args, value) => {1085 callback: (_args, value) => {
1085 const details = getReasoningDetailsElements(value);1086 const details = getReasoningDetailsElements(value.toString());
1086 if (details) details.removeAttr('open');1087 if (details) details.removeAttr('open');
1087 return '';1088 return '';
1088 },1089 },
@@ -1094,7 +1095,7 @@ function registerReasoningSlashCommands() {
1094 helpString: t`Expand the reasoning block of a message or range of messages.`,1095 helpString: t`Expand the reasoning block of a message or range of messages.`,
1095 unnamedArgumentList: reasoningVisibilityArgs,1096 unnamedArgumentList: reasoningVisibilityArgs,
1096 callback: (_args, value) => {1097 callback: (_args, value) => {
1097 const details = getReasoningDetailsElements(value);1098 const details = getReasoningDetailsElements(value.toString());
1098 if (details) details.attr('open', '');1099 if (details) details.attr('open', '');
1099 return '';1100 return '';
1100 },1101 },
@@ -1106,7 +1107,7 @@ function registerReasoningSlashCommands() {
1106 helpString: t`Toggle the reasoning block of a message or range of messages. Expanded blocks will be collapsed, and collapsed blocks will be expanded.`,1107 helpString: t`Toggle the reasoning block of a message or range of messages. Expanded blocks will be collapsed, and collapsed blocks will be expanded.`,
1107 unnamedArgumentList: reasoningVisibilityArgs,1108 unnamedArgumentList: reasoningVisibilityArgs,
1108 callback: (_args, value) => {1109 callback: (_args, value) => {
1109 const details = getReasoningDetailsElements(value);1110 const details = getReasoningDetailsElements(value.toString());
1110 if (!details) return '';1111 if (!details) return '';
1111 details.each(function () {1112 details.each(function () {
1112 const $el = $(this);1113 const $el = $(this);
public/scripts/secrets.js+3 -0
@@ -76,6 +76,7 @@ export const SECRET_KEYS = {
76 POLLINATIONS: 'api_key_pollinations',76 POLLINATIONS: 'api_key_pollinations',
77 VOLCENGINE_APP_ID: 'volcengine_app_id',77 VOLCENGINE_APP_ID: 'volcengine_app_id',
78 VOLCENGINE_ACCESS_KEY: 'volcengine_access_key',78 VOLCENGINE_ACCESS_KEY: 'volcengine_access_key',
79 WORKERS_AI: 'api_key_workers_ai',
79};80};
8081
81const FRIENDLY_NAMES = {82const FRIENDLY_NAMES = {
@@ -140,6 +141,7 @@ const FRIENDLY_NAMES = {
140 [SECRET_KEYS.POLLINATIONS]: 'Pollinations',141 [SECRET_KEYS.POLLINATIONS]: 'Pollinations',
141 [SECRET_KEYS.VOLCENGINE_APP_ID]: 'Volcengine App ID',142 [SECRET_KEYS.VOLCENGINE_APP_ID]: 'Volcengine App ID',
142 [SECRET_KEYS.VOLCENGINE_ACCESS_KEY]: 'Volcengine Access Key',143 [SECRET_KEYS.VOLCENGINE_ACCESS_KEY]: 'Volcengine Access Key',
144 [SECRET_KEYS.WORKERS_AI]: 'Cloudflare Workers AI',
143};145};
144146
145const INPUT_MAP = {147const INPUT_MAP = {
@@ -184,6 +186,7 @@ const INPUT_MAP = {
184 [SECRET_KEYS.SILICONFLOW]: '#api_key_siliconflow',186 [SECRET_KEYS.SILICONFLOW]: '#api_key_siliconflow',
185 [SECRET_KEYS.COMFY_RUNPOD]: '#api_key_comfy_runpod',187 [SECRET_KEYS.COMFY_RUNPOD]: '#api_key_comfy_runpod',
186 [SECRET_KEYS.POLLINATIONS]: '#api_key_pollinations',188 [SECRET_KEYS.POLLINATIONS]: '#api_key_pollinations',
189 [SECRET_KEYS.WORKERS_AI]: '#api_key_workers_ai',
187};190};
188191
189const getLabel = () => moment().format('L LT');192const getLabel = () => moment().format('L LT');
public/scripts/slash-commands.js+1 -0
@@ -6386,6 +6386,7 @@ function getModelOptions(quiet) {
6386 { id: 'model_fireworks_select', api: 'openai', type: chat_completion_sources.FIREWORKS },6386 { id: 'model_fireworks_select', api: 'openai', type: chat_completion_sources.FIREWORKS },
6387 { id: 'model_cometapi_select', api: 'openai', type: chat_completion_sources.COMETAPI },6387 { id: 'model_cometapi_select', api: 'openai', type: chat_completion_sources.COMETAPI },
6388 { id: 'model_zai_select', api: 'openai', type: chat_completion_sources.ZAI },6388 { 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 },
6389 { id: 'model_novel_select', api: 'novel', type: null },6390 { id: 'model_novel_select', api: 'novel', type: null },
6390 { id: 'horde_model', api: 'koboldhorde', type: null },6391 { id: 'horde_model', api: 'koboldhorde', type: null },
6391 ];6392 ];
public/scripts/tokenizers.js+22 -0
@@ -694,6 +694,28 @@ export function getTokenizerModel() {
694 }694 }
695 }695 }
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
697 if (oai_settings.chat_completion_source == chat_completion_sources.COHERE) {719 if (oai_settings.chat_completion_source == chat_completion_sources.COHERE) {
698 if (oai_settings.cohere_model.includes('command-a')) {720 if (oai_settings.cohere_model.includes('command-a')) {
699 return commandATokenizer;721 return commandATokenizer;
public/scripts/tool-calling.js+3 -0
@@ -637,6 +637,8 @@ export class ToolManager {
637 return currentModel.supported_features?.includes('tools');637 return currentModel.supported_features?.includes('tools');
638 case chat_completion_sources.ELECTRONHUB:638 case chat_completion_sources.ELECTRONHUB:
639 return currentModel.metadata?.function_call;639 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');
640 }642 }
641 }643 }
642644
@@ -664,6 +666,7 @@ export class ToolManager {
664 chat_completion_sources.ZAI,666 chat_completion_sources.ZAI,
665 chat_completion_sources.SILICONFLOW,667 chat_completion_sources.SILICONFLOW,
666 chat_completion_sources.NANOGPT,668 chat_completion_sources.NANOGPT,
669 chat_completion_sources.WORKERS_AI,
667 ];670 ];
668 return supportedSources.includes(settings.chat_completion_source);671 return supportedSources.includes(settings.chat_completion_source);
669 }672 }
src/constants.js+1 -0
@@ -209,6 +209,7 @@ export const CHAT_COMPLETION_SOURCES = {
209 AZURE_OPENAI: 'azure_openai',209 AZURE_OPENAI: 'azure_openai',
210 ZAI: 'zai',210 ZAI: 'zai',
211 SILICONFLOW: 'siliconflow',211 SILICONFLOW: 'siliconflow',
212 WORKERS_AI: 'workers_ai',
212};213};
213214
214/**215/**
src/endpoints/backends/chat-completions.js+62 -0
@@ -90,6 +90,7 @@ const API_ZAI_CODING = 'https://api.z.ai/api/coding/paas/v4';
90const API_SILICONFLOW = 'https://api.siliconflow.com/v1';90const API_SILICONFLOW = 'https://api.siliconflow.com/v1';
91const API_SILICONFLOW_CN = 'https://api.siliconflow.cn/v1';91const API_SILICONFLOW_CN = 'https://api.siliconflow.cn/v1';
92const API_OPENROUTER = 'https://openrouter.ai/api/v1';92const API_OPENROUTER = 'https://openrouter.ai/api/v1';
93const API_WORKERS_AI = 'https://api.cloudflare.com/client/v4/accounts';
9394
94/**95/**
95 * Module-scoped Claude caching configuration values.96 * Module-scoped Claude caching configuration values.
@@ -1833,6 +1834,49 @@ router.post('/status', async function (request, statusResponse) {
1833 apiKey = readSecret(request.user.directories, SECRET_KEYS.SILICONFLOW, request.body.secret_id);1834 apiKey = readSecret(request.user.directories, SECRET_KEYS.SILICONFLOW, request.body.secret_id);
1834 headers = {};1835 headers = {};
1835 queryParams = { type: 'text', sub_type: 'chat' };1836 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 }
1836 } else {1880 } else {
1837 console.warn('This chat completion source is not supported yet.');1881 console.warn('This chat completion source is not supported yet.');
1838 return statusResponse.status(400).send({ error: true });1882 return statusResponse.status(400).send({ error: true });
@@ -2315,6 +2359,24 @@ router.post('/generate', async function (request, response) {
2315 if (request.body.json_schema) {2359 if (request.body.json_schema) {
2316 setJsonObjectFormat(bodyParams, request.body.messages, request.body.json_schema);2360 setJsonObjectFormat(bodyParams, request.body.messages, request.body.json_schema);
2317 }2361 }
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 }
2318 } else {2380 } else {
2319 console.warn('This chat completion source is not supported yet.');2381 console.warn('This chat completion source is not supported yet.');
2320 return response.status(400).send({ error: true });2382 return response.status(400).send({ error: true });
src/endpoints/secrets.js+1 -0
@@ -69,6 +69,7 @@ export const SECRET_KEYS = {
69 POLLINATIONS: 'api_key_pollinations',69 POLLINATIONS: 'api_key_pollinations',
70 VOLCENGINE_APP_ID: 'volcengine_app_id',70 VOLCENGINE_APP_ID: 'volcengine_app_id',
71 VOLCENGINE_ACCESS_KEY: 'volcengine_access_key',71 VOLCENGINE_ACCESS_KEY: 'volcengine_access_key',
72 WORKERS_AI: 'api_key_workers_ai',
72};73};
7374
74/**75/**