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