feat: Add Azure OpenAI chat completions provider (#4456) * feat: Add Azure OpenAI chat completions provider This commit introduces comprehensive support for integrating Azure OpenAI as a new chat completion source within SillyTavern. The implementation specifically navigates Azure's reliance on deployment names, differing from standard model selection. Includes: - **New Feat / UI:** Dedicated settings in `public/index.html` for Azure parameters (Base URL, Deployment Name, API Version, API Key) and a new `public/img/azure_openai.svg` icon. - **Frontend Logic (`public/scripts/openai.js`):** Manages Azure OpenAI settings and dynamic URL generation. The 'Connect' button triggers a `GET /models` endpoint call solely for API configuration validation (key, URL, version). Upon successful validation, a subsequent `POST` request is made to the AI, and the active model is extracted from its response payload, then populated for UI display (no direct user model selection). - **Backend Logic (`src/endpoints/backends/chat-completions.js`):** Implements `sendAzureOpenAIRequest` to proxy requests, handling Azure’s unique authentication and URL structures. Enhanced the `/status` endpoint for Azure-specific validation. - **Secret Management:** Secure handling of Azure OpenAI API keys in `public/scripts/secrets.js` and `src/endpoints/secrets.js`. - **Autoconnect:** `public/scripts/RossAscends-mods.js` modified to enable automatic connection for Azure OpenAI if an API key is present. Impact: - Users can now connect to and utilize Azure OpenAI services. - Requires new configuration details in the UI. - Enhances API routing and validation with Azure-specific behavior. - Navigates model handling: model name is *derived dynamically* from AI response, not directly selected, aligning with internal SillyTavern structure. * fixed the html issues, openai.ujs and secrets.js modified: public/index.html# modified: public/scripts/openai.js modified: public/scripts/secrets.js * Extends Azure OpenAI with advanced capabilities Enables advanced features such as function calling, image inlining, and model reasoning in the UI for Azure OpenAI. Refactors the backend to support structured output (JSON mode), native thinking, and reasoning effort for compatible Azure OpenAI models. Adds support for logprobs, multiple responses (`n`), and seed, aligning with standard OpenAI behavior. Improves request handling with a retry mechanism for rate limits and more robust error reporting. Ensures correct parameter handling for model-specific features and conflicts, such as when native thinking is enabled. Optimizes the Azure OpenAI status probe for efficiency. modified: public/index.html modified: public/scripts/openai.js modified: src/endpoints/backends/chat-completions.js * PR fixes Simplification Removed reasoning logic from backend modified: src/endpoints/backends/chat-completions.js modified: src/endpoints/backends/chat-completions.js * Fix PR comments Removed the front end compiled url UI element and related. Misc simplifications modified: public/index.html modified: public/scripts/openai.js modified: src/endpoints/backends/chat-completions.js * Fixed accidental api temporary disabled modified: public/index.html * Don't transfer status code verbatim * Fix formatting * Enable tool calling * Fix logo coloration * Move model arrays to shared constants * Fix capitalization * Remove obsolete comment * Improve response schema parameter format * Fix tokenizer model selection --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>

1147507d06de13ecb137294a7e9156fc4698f288

nfuller286 <80130411+nfuller286@users.noreply.github.com>

Signed
11 files changed, +412 -46Ignore whitespace
public/img/azure_openai.svg+1 -0
@@ -0,0 +1 @@
1<svg id="uuid-adbdae8e-5a41-46d1-8c18-aa73cdbfee32" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18" height="100px" width="100px" transform="rotate(0) scale(1, 1)"><path d="m0,2.7v12.6c0,1.491,1.209,2.7,2.7,2.7h12.6c1.491,0,2.7-1.209,2.7-2.7V2.7c0-1.491-1.209-2.7-2.7-2.7H2.7C1.209,0,0,1.209,0,2.7ZM10.8,0v3.6c0,3.976,3.224,7.2,7.2,7.2h-3.6c-3.976,0-7.199,3.222-7.2,7.198v-3.598c0-3.976-3.224-7.2-7.2-7.2h3.6c3.976,0,7.2-3.224,7.2-7.2Z" stroke-width="0"/></svg>
public/index.html+57 -13
@@ -652,7 +652,7 @@
652 <input type="number" id="openai_max_tokens" name="openai_max_tokens" class="text_pole" min="1" max="65536">652 <input type="number" id="openai_max_tokens" name="openai_max_tokens" class="text_pole" min="1" max="65536">
653 </div>653 </div>
654 </div>654 </div>
655 <div class="range-block" data-source="openai,custom,xai,aimlapi,moonshot">655 <div class="range-block" data-source="openai,custom,xai,aimlapi,moonshot,azure_openai">
656 <div class="range-block-title" data-i18n="Multiple swipes per generation">656 <div class="range-block-title" data-i18n="Multiple swipes per generation">
657 Multiple swipes per generation657 Multiple swipes per generation
658 </div>658 </div>
@@ -691,7 +691,7 @@
691 </span>691 </span>
692 </div>692 </div>
693 </div>693 </div>
694 <div class="range-block" data-source="openai,claude,aimlapi,openrouter,ai21,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,electronhub,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi">694 <div class="range-block" data-source="openai,claude,aimlapi,openrouter,ai21,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,electronhub,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai">
695 <div class="range-block-title" data-i18n="Temperature">695 <div class="range-block-title" data-i18n="Temperature">
696 Temperature696 Temperature
697 </div>697 </div>
@@ -704,7 +704,7 @@
704 </div>704 </div>
705 </div>705 </div>
706 </div>706 </div>
707 <div class="range-block" data-source="openai,aimlapi,openrouter,custom,cohere,perplexity,groq,mistralai,electronhub,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi">707 <div class="range-block" data-source="openai,aimlapi,openrouter,custom,cohere,perplexity,groq,mistralai,electronhub,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai">
708 <div class="range-block-title" data-i18n="Frequency Penalty">708 <div class="range-block-title" data-i18n="Frequency Penalty">
709 Frequency Penalty709 Frequency Penalty
710 </div>710 </div>
@@ -717,7 +717,7 @@
717 </div>717 </div>
718 </div>718 </div>
719 </div>719 </div>
720 <div class="range-block" data-source="openai,aimlapi,openrouter,custom,cohere,perplexity,groq,mistralai,electronhub,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi">720 <div class="range-block" data-source="openai,aimlapi,openrouter,custom,cohere,perplexity,groq,mistralai,electronhub,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai">
721 <div class="range-block-title" data-i18n="Presence Penalty">721 <div class="range-block-title" data-i18n="Presence Penalty">
722 Presence Penalty722 Presence Penalty
723 </div>723 </div>
@@ -743,7 +743,7 @@
743 </div>743 </div>
744 </div>744 </div>
745 </div>745 </div>
746 <div class="range-block" data-source="openai,claude,aimlapi,openrouter,ai21,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,electronhub,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi">746 <div class="range-block" data-source="openai,claude,aimlapi,openrouter,ai21,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,electronhub,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai">
747 <div class="range-block-title" data-i18n="Top P">747 <div class="range-block-title" data-i18n="Top P">
748 Top P748 Top P
749 </div>749 </div>
@@ -980,7 +980,7 @@
980 </div>980 </div>
981 </div>981 </div>
982 </div>982 </div>
983 <div class="range-block" data-source="openai,openrouter,mistralai,custom,cohere,groq,electronhub,nanogpt,xai,pollinations,aimlapi,makersuite,vertexai">983 <div class="range-block" data-source="openai,openrouter,mistralai,custom,cohere,groq,electronhub,nanogpt,xai,pollinations,aimlapi,makersuite,vertexai,azure_openai">
984 <div class="range-block-title justifyLeft" data-i18n="Seed">984 <div class="range-block-title justifyLeft" data-i18n="Seed">
985 Seed985 Seed
986 </div>986 </div>
@@ -1984,7 +1984,7 @@
1984 </b>1984 </b>
1985 </div>1985 </div>
1986 </div>1986 </div>
1987 <div class="range-block" data-source="openai,cohere,mistralai,custom,claude,aimlapi,openrouter,groq,deepseek,makersuite,vertexai,ai21,xai,pollinations,moonshot,fireworks,cometapi,electronhub">1987 <div class="range-block" data-source="openai,cohere,mistralai,custom,claude,aimlapi,openrouter,groq,deepseek,makersuite,vertexai,ai21,xai,pollinations,moonshot,fireworks,cometapi,electronhub,azure_openai">
1988 <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">1988 <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">
1989 <input id="openai_function_calling" type="checkbox" />1989 <input id="openai_function_calling" type="checkbox" />
1990 <span data-i18n="Enable function calling">Enable function calling</span>1990 <span data-i18n="Enable function calling">Enable function calling</span>
@@ -1999,7 +1999,7 @@
1999 <strong data-i18n="enable_functions_desc_4">Not supported when Prompt Post-Processing with "no tools" is used!</strong>1999 <strong data-i18n="enable_functions_desc_4">Not supported when Prompt Post-Processing with "no tools" is used!</strong>
2000 </div>2000 </div>
2001 </div>2001 </div>
2002 <div class="range-block" data-source="openai,aimlapi,openrouter,mistralai,makersuite,vertexai,claude,custom,xai,pollinations,moonshot,cohere,cometapi,nanogpt,electronhub">2002 <div class="range-block" data-source="openai,aimlapi,openrouter,mistralai,makersuite,vertexai,claude,custom,xai,pollinations,moonshot,cohere,cometapi,nanogpt,electronhub,azure_openai">
2003 <label for="openai_image_inlining" class="checkbox_label flexWrap widthFreeExpand">2003 <label for="openai_image_inlining" class="checkbox_label flexWrap widthFreeExpand">
2004 <input id="openai_image_inlining" type="checkbox" />2004 <input id="openai_image_inlining" type="checkbox" />
2005 <span data-i18n="Send inline images">Send inline images</span>2005 <span data-i18n="Send inline images">Send inline images</span>
@@ -2015,7 +2015,7 @@
2015 <code><i class="fa-solid fa-wand-magic-sparkles"></i></code>2015 <code><i class="fa-solid fa-wand-magic-sparkles"></i></code>
2016 <span data-i18n="image_inlining_hint_3">menu to attach an image file to the chat.</span>2016 <span data-i18n="image_inlining_hint_3">menu to attach an image file to the chat.</span>
2017 </div>2017 </div>
2018 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,xai,pollinations,cohere,cometapi,nanogpt,moonshot,aimlapi,openrouter,mistralai,electronhub">2018 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,xai,pollinations,cohere,cometapi,nanogpt,moonshot,aimlapi,openrouter,mistralai,electronhub,azure_openai">
2019 <div class="flex-container oneline-dropdown">2019 <div class="flex-container oneline-dropdown">
2020 <label for="openai_inline_image_quality" data-i18n="Inline Image Quality">2020 <label for="openai_inline_image_quality" data-i18n="Inline Image Quality">
2021 Inline Image Quality2021 Inline Image Quality
@@ -2077,7 +2077,7 @@
2077 </span>2077 </span>
2078 </div>2078 </div>
2079 </div>2079 </div>
2080 <div class="range-block" data-source="deepseek,aimlapi,openrouter,custom,claude,xai,makersuite,vertexai,pollinations,moonshot,mistralai,fireworks,cometapi,electronhub">2080 <div class="range-block" data-source="deepseek,aimlapi,openrouter,custom,claude,xai,makersuite,vertexai,pollinations,moonshot,mistralai,fireworks,cometapi,electronhub,azure_openai">
2081 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">2081 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">
2082 <input id="openai_show_thoughts" type="checkbox" />2082 <input id="openai_show_thoughts" type="checkbox" />
2083 <span data-i18n="Request model reasoning">Request model reasoning</span>2083 <span data-i18n="Request model reasoning">Request model reasoning</span>
@@ -2091,7 +2091,7 @@
2091 </span>2091 </span>
2092 </div>2092 </div>
2093 </div>2093 </div>
2094 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,claude,xai,makersuite,vertexai,aimlapi,openrouter,pollinations,perplexity,cometapi,electronhub">2094 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,claude,xai,makersuite,vertexai,aimlapi,openrouter,pollinations,perplexity,cometapi,electronhub,azure_openai">
2095 <div class="flex-container oneline-dropdown" title="Constrains effort on reasoning for reasoning models.&#10;Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response." data-i18n="[title]Constrains effort on reasoning for reasoning models.">2095 <div class="flex-container oneline-dropdown" title="Constrains effort on reasoning for reasoning models.&#10;Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response." data-i18n="[title]Constrains effort on reasoning for reasoning models.">
2096 <label for="openai_reasoning_effort">2096 <label for="openai_reasoning_effort">
2097 <span data-i18n="Reasoning Effort">Reasoning Effort</span>2097 <span data-i18n="Reasoning Effort">Reasoning Effort</span>
@@ -2105,7 +2105,7 @@
2105 <option data-i18n="openai_reasoning_effort_high" value="high">High</option>2105 <option data-i18n="openai_reasoning_effort_high" value="high">High</option>
2106 <option data-i18n="openai_reasoning_effort_maximum" value="max">Maximum</option>2106 <option data-i18n="openai_reasoning_effort_maximum" value="max">Maximum</option>
2107 </select>2107 </select>
2108 <div class="toggle-description justifyLeft marginBot5" data-source="openai,custom,xai,aimlapi,openrouter,perplexity,electronhub" data-i18n="OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level.">2108 <div class="toggle-description justifyLeft marginBot5" data-source="openai,custom,xai,aimlapi,openrouter,perplexity,electronhub,azure_openai" data-i18n="OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level.">
2109 OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level.2109 OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level.
2110 </div>2110 </div>
2111 <div class="toggle-description justifyLeft marginBot5" data-source="claude" data-i18n="Allocates a portion of the response length for thinking (min: 1024 tokens, low: 10%, medium: 25%, high: 50%, max: 95%), but minimum 1024 tokens. Auto does not request thinking.">2111 <div class="toggle-description justifyLeft marginBot5" data-source="claude" data-i18n="Allocates a portion of the response length for thinking (min: 1024 tokens, low: 10%, medium: 25%, high: 50%, max: 95%), but minimum 1024 tokens. Auto does not request thinking.">
@@ -2144,7 +2144,7 @@
2144 </div>2144 </div>
2145 </div>2145 </div>
2146 </div>2146 </div>
2147 <div class="range-block m-t-1" data-source="openai,aimlapi,openrouter,custom">2147 <div class="range-block m-t-1" data-source="openai,aimlapi,openrouter,custom,azure_openai">
2148 <div id="logit_bias_openai" class="range-block-title openai_restorable" data-i18n="Logit Bias">2148 <div id="logit_bias_openai" class="range-block-title openai_restorable" data-i18n="Logit Bias">
2149 Logit Bias2149 Logit Bias
2150 </div>2150 </div>
@@ -2802,6 +2802,7 @@
2802 <optgroup>2802 <optgroup>
2803 <option value="ai21">AI21</option>2803 <option value="ai21">AI21</option>
2804 <option value="aimlapi">AI/ML API</option>2804 <option value="aimlapi">AI/ML API</option>
2805 <option value="azure_openai">Azure OpenAI</option>
2805 <option value="claude">Claude</option>2806 <option value="claude">Claude</option>
2806 <option value="cohere">Cohere</option>2807 <option value="cohere">Cohere</option>
2807 <!-- Temporarily disabled. -->2808 <!-- Temporarily disabled. -->
@@ -3735,6 +3736,49 @@
3735 <option value="kimi-thinking-preview">kimi-thinking-preview</option>3736 <option value="kimi-thinking-preview">kimi-thinking-preview</option>
3736 </select>3737 </select>
3737 </div>3738 </div>
3739 <div id="azure_openai_settings" data-source="azure_openai">
3740 <!-- Azure Base URL -->
3741 <h4><span data-i18n="Azure Base URL">Azure Base URL</span></h4>
3742 <div class="flex-container">
3743 <input id="azure_base_url" data-setting="azure_base_url" class="text_pole wide100p" type="text" placeholder="https://your-resource.openai.azure.com/">
3744 </div>
3745
3746 <!-- Azure Deployment Name -->
3747 <h4><span data-i18n="Deployment Name">Deployment Name</span></h4>
3748 <div class="flex-container">
3749 <input id="azure_deployment_name" data-setting="azure_deployment_name" class="text_pole wide100p" type="text" placeholder="your-deployment-name" title="The name of your model deployment in Azure." data-i18n="[title]The name of your model deployment in Azure.">
3750 </div>
3751
3752 <!-- Azure API Version Dropdown -->
3753 <h4><span data-i18n="API Version">API Version</span></h4>
3754 <div class="flex-container">
3755 <select id="azure_api_version" data-setting="azure_api_version" class="text_pole wide100p">
3756 <option value="2025-04-01-preview">2025-04-01-preview</option>
3757 <option value="2024-10-21">2024-10-21</option>
3758 </select>
3759 </div>
3760
3761 <!-- Azure API Key -->
3762 <h4><span data-i18n="Azure API Key">Azure API Key</span></h4>
3763 <div class="flex-container">
3764 <input id="api_key_azure_openai" data-setting="api_key_azure_openai" class="text_pole flex1" type="password" autocomplete="off">
3765 <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_azure_openai"></div>
3766 </div>
3767 <div class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you click 'Connect'." data-for="api_key_azure_openai">
3768 For privacy reasons, your API key will be hidden after you click 'Connect'.
3769 </div>
3770
3771 <!-- Model Name (Select) -->
3772 <h4><span data-i18n="Model Name">Model Name</span></h4>
3773 <div class="flex-container">
3774 <select id="azure_openai_model" data-setting="azure_openai_model" class="text_pole wide100p">
3775 <option value="" disabled selected data-i18n="Click 'Connect' to fetch model name">Click 'Connect' to fetch model name</option>
3776 </select>
3777 </div>
3778 <div>
3779 <small data-i18n="The underlying model of your deployment. This is detected automatically when you connect.">The underlying model of your deployment. This is detected automatically when you connect.</small>
3780 </div>
3781 </div>
3738 <div id="prompt_post_processing_form">3782 <div id="prompt_post_processing_form">
3739 <h4>3783 <h4>
3740 <span data-i18n="Prompt Post-Processing">3784 <span data-i18n="Prompt Post-Processing">
public/script.js+1 -0
@@ -5370,6 +5370,7 @@ export function extractJsonFromData(data, { mainApi = null, chatCompletionSource
5370 case chat_completion_sources.COHERE:5370 case chat_completion_sources.COHERE:
5371 case chat_completion_sources.XAI:5371 case chat_completion_sources.XAI:
5372 case chat_completion_sources.ELECTRONHUB:5372 case chat_completion_sources.ELECTRONHUB:
5373 case chat_completion_sources.AZURE_OPENAI:
5373 default:5374 default:
5374 result = tryParse(text);5375 result = tryParse(text);
5375 break;5376 break;
public/scripts/RossAscends-mods.js+1 -0
@@ -412,6 +412,7 @@ function RA_autoconnect(PrevApi) {
412 || (secret_state[SECRET_KEYS.COMETAPI] && oai_settings.chat_completion_source == chat_completion_sources.COMETAPI)412 || (secret_state[SECRET_KEYS.COMETAPI] && oai_settings.chat_completion_source == chat_completion_sources.COMETAPI)
413 || (oai_settings.chat_completion_source === chat_completion_sources.POLLINATIONS)413 || (oai_settings.chat_completion_source === chat_completion_sources.POLLINATIONS)
414 || (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM)414 || (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM)
415 || (secret_state[SECRET_KEYS.AZURE_OPENAI] && oai_settings.chat_completion_source == chat_completion_sources.AZURE_OPENAI)
415 ) {416 ) {
416 $('#api_button_openai').trigger('click');417 $('#api_button_openai').trigger('click');
417 }418 }
public/scripts/openai.js+125 -13
@@ -188,6 +188,7 @@ export const chat_completion_sources = {
188 MOONSHOT: 'moonshot',188 MOONSHOT: 'moonshot',
189 FIREWORKS: 'fireworks',189 FIREWORKS: 'fireworks',
190 COMETAPI: 'cometapi',190 COMETAPI: 'cometapi',
191 AZURE_OPENAI: 'azure_openai',
191};192};
192193
193const character_names_behavior = {194const character_names_behavior = {
@@ -241,6 +242,8 @@ const sensitiveFields = [
241 'custom_include_headers',242 'custom_include_headers',
242 'vertexai_region',243 'vertexai_region',
243 'vertexai_express_project_id',244 'vertexai_express_project_id',
245 'azure_base_url',
246 'azure_deployment_name',
244];247];
245248
246/**249/**
@@ -331,6 +334,10 @@ export const settingsToUpdate = {
331 n: ['#n_openai', 'n', false, false],334 n: ['#n_openai', 'n', false, false],
332 bypass_status_check: ['#openai_bypass_status_check', 'bypass_status_check', true, true],335 bypass_status_check: ['#openai_bypass_status_check', 'bypass_status_check', true, true],
333 request_images: ['#openai_request_images', 'request_images', true, false],336 request_images: ['#openai_request_images', 'request_images', true, false],
337 azure_base_url: ['#azure_base_url', 'azure_base_url', false, true],
338 azure_deployment_name: ['#azure_deployment_name', 'azure_deployment_name', false, true],
339 azure_api_version: ['#azure_api_version', 'azure_api_version', false, true],
340 azure_openai_model: ['#azure_openai_model', 'azure_openai_model', false, true],
334 extensions: ['#NULL_SELECTOR', 'extensions', false, false],341 extensions: ['#NULL_SELECTOR', 'extensions', false, false],
335};342};
336343
@@ -380,6 +387,10 @@ const default_settings = {
380 cometapi_model: 'gpt-4o',387 cometapi_model: 'gpt-4o',
381 moonshot_model: 'kimi-latest',388 moonshot_model: 'kimi-latest',
382 fireworks_model: 'accounts/fireworks/models/kimi-k2-instruct',389 fireworks_model: 'accounts/fireworks/models/kimi-k2-instruct',
390 azure_base_url: '',
391 azure_deployment_name: '',
392 azure_api_version: '2024-02-15-preview',
393 azure_openai_model: '',
383 custom_model: '',394 custom_model: '',
384 custom_url: '',395 custom_url: '',
385 custom_include_body: '',396 custom_include_body: '',
@@ -470,6 +481,10 @@ const oai_settings = {
470 cometapi_model: 'gpt-4o',481 cometapi_model: 'gpt-4o',
471 moonshot_model: 'kimi-latest',482 moonshot_model: 'kimi-latest',
472 fireworks_model: 'accounts/fireworks/models/kimi-k2-instruct',483 fireworks_model: 'accounts/fireworks/models/kimi-k2-instruct',
484 azure_base_url: '',
485 azure_deployment_name: '',
486 azure_api_version: '2024-02-15-preview',
487 azure_openai_model: '',
473 custom_model: '',488 custom_model: '',
474 custom_url: '',489 custom_url: '',
475 custom_include_body: '',490 custom_include_body: '',
@@ -1641,6 +1656,8 @@ export function getChatCompletionModel(source = null) {
1641 return oai_settings.moonshot_model;1656 return oai_settings.moonshot_model;
1642 case chat_completion_sources.FIREWORKS:1657 case chat_completion_sources.FIREWORKS:
1643 return oai_settings.fireworks_model;1658 return oai_settings.fireworks_model;
1659 case chat_completion_sources.AZURE_OPENAI:
1660 return oai_settings.azure_openai_model;
1644 default:1661 default:
1645 console.error(`Unknown chat completion source: ${activeSource}`);1662 console.error(`Unknown chat completion source: ${activeSource}`);
1646 return '';1663 return '';
@@ -1957,6 +1974,16 @@ function saveModelList(data) {
19571974
1958 $('#model_cometapi_select').val(oai_settings.cometapi_model).trigger('change');1975 $('#model_cometapi_select').val(oai_settings.cometapi_model).trigger('change');
1959 }1976 }
1977
1978 if (oai_settings.chat_completion_source == chat_completion_sources.AZURE_OPENAI) {
1979 const modelId = model_list?.[0]?.id || '';
1980 oai_settings.azure_openai_model = modelId;
1981
1982 $('#azure_openai_model')
1983 .empty()
1984 .append(new Option(modelId || 'None', modelId || '', true, true))
1985 .trigger('change');
1986 }
1960}1987}
19611988
1962function appendOpenRouterOptions(model_list, groupModels = false, sort = false) {1989function appendOpenRouterOptions(model_list, groupModels = false, sort = false) {
@@ -2068,6 +2095,7 @@ function getReasoningEffort() {
2068 // These sources expect the effort as string.2095 // These sources expect the effort as string.
2069 const reasoningEffortSources = [2096 const reasoningEffortSources = [
2070 chat_completion_sources.OPENAI,2097 chat_completion_sources.OPENAI,
2098 chat_completion_sources.AZURE_OPENAI,
2071 chat_completion_sources.CUSTOM,2099 chat_completion_sources.CUSTOM,
2072 chat_completion_sources.XAI,2100 chat_completion_sources.XAI,
2073 chat_completion_sources.AIMLAPI,2101 chat_completion_sources.AIMLAPI,
@@ -2087,7 +2115,7 @@ function getReasoningEffort() {
2087 case reasoning_effort_types.auto:2115 case reasoning_effort_types.auto:
2088 return undefined;2116 return undefined;
2089 case reasoning_effort_types.min:2117 case reasoning_effort_types.min:
2090 return chat_completion_sources.OPENAI === oai_settings.chat_completion_source && /^gpt-5/.test(oai_settings.openai_model)2118 return [chat_completion_sources.OPENAI, chat_completion_sources.AZURE_OPENAI].includes(oai_settings.chat_completion_source) && /^gpt-5/.test(getChatCompletionModel())
2091 ? reasoning_effort_types.min2119 ? reasoning_effort_types.min
2092 : reasoning_effort_types.low;2120 : reasoning_effort_types.low;
2093 case reasoning_effort_types.max:2121 case reasoning_effort_types.max:
@@ -2154,15 +2182,16 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } =
2154 const isXAI = oai_settings.chat_completion_source == chat_completion_sources.XAI;2182 const isXAI = oai_settings.chat_completion_source == chat_completion_sources.XAI;
2155 const isPollinations = oai_settings.chat_completion_source == chat_completion_sources.POLLINATIONS;2183 const isPollinations = oai_settings.chat_completion_source == chat_completion_sources.POLLINATIONS;
2156 const isMoonshot = oai_settings.chat_completion_source == chat_completion_sources.MOONSHOT;2184 const isMoonshot = oai_settings.chat_completion_source == chat_completion_sources.MOONSHOT;
2185 const isAzureOpenAI = oai_settings.chat_completion_source == chat_completion_sources.AZURE_OPENAI; // Add this line
2157 const isTextCompletion = isOAI && textCompletionModels.includes(oai_settings.openai_model);2186 const isTextCompletion = isOAI && textCompletionModels.includes(oai_settings.openai_model);
2158 const isQuiet = type === 'quiet';2187 const isQuiet = type === 'quiet';
2159 const isImpersonate = type === 'impersonate';2188 const isImpersonate = type === 'impersonate';
2160 const isContinue = type === 'continue';2189 const isContinue = type === 'continue';
2161 const stream = oai_settings.stream_openai && !isQuiet && !(isOAI && ['o1-2024-12-17', 'o1'].includes(oai_settings.openai_model));2190 const stream = oai_settings.stream_openai && !isQuiet && !((isOAI || isAzureOpenAI) && ['o1-2024-12-17', 'o1'].includes(getChatCompletionModel()));
2162 const useLogprobs = !!power_user.request_token_probabilities;2191 const useLogprobs = !!power_user.request_token_probabilities;
2163 const canMultiSwipe = oai_settings.n > 1 && !isContinue && !isImpersonate && !isQuiet && (isOAI || isCustom || isXAI || isAimlapi || isMoonshot);2192 const canMultiSwipe = oai_settings.n > 1 && !isContinue && !isImpersonate && !isQuiet && (isOAI || isAzureOpenAI || isCustom || isXAI || isAimlapi || isMoonshot);
21642193
2165 const logitBiasSources = [chat_completion_sources.OPENAI, chat_completion_sources.OPENROUTER, chat_completion_sources.CUSTOM];2194 const logitBiasSources = [chat_completion_sources.OPENAI, chat_completion_sources.AZURE_OPENAI, chat_completion_sources.OPENROUTER, chat_completion_sources.CUSTOM];
2166 if (oai_settings.bias_preset_selected2195 if (oai_settings.bias_preset_selected
2167 && logitBiasSources.includes(oai_settings.chat_completion_source)2196 && logitBiasSources.includes(oai_settings.chat_completion_source)
2168 && Array.isArray(oai_settings.bias_presets[oai_settings.bias_preset_selected])2197 && Array.isArray(oai_settings.bias_presets[oai_settings.bias_preset_selected])
@@ -2200,6 +2229,16 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } =
2200 'custom_prompt_post_processing': oai_settings.custom_prompt_post_processing,2229 'custom_prompt_post_processing': oai_settings.custom_prompt_post_processing,
2201 };2230 };
22022231
2232 if (isAzureOpenAI) {
2233 generate_data.azure_base_url = oai_settings.azure_base_url;
2234 generate_data.azure_deployment_name = oai_settings.azure_deployment_name;
2235 generate_data.azure_api_version = oai_settings.azure_api_version;
2236 // Reasoning effort is not supported on some Azure models (e.g. GPT-3.x, GPT-4.x)
2237 if (/^gpt-[34]/.test(oai_settings.azure_openai_model)) {
2238 delete generate_data.reasoning_effort;
2239 }
2240 }
2241
2203 if (!canMultiSwipe && ToolManager.canPerformToolCalls(type)) {2242 if (!canMultiSwipe && ToolManager.canPerformToolCalls(type)) {
2204 await ToolManager.registerFunctionToolsOpenAI(generate_data);2243 await ToolManager.registerFunctionToolsOpenAI(generate_data);
2205 }2244 }
@@ -2217,18 +2256,18 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } =
2217 }2256 }
22182257
2219 // Add logprobs request (currently OpenAI only, max 5 on their side)2258 // Add logprobs request (currently OpenAI only, max 5 on their side)
2220 if (useLogprobs && (isOAI || isCustom || isDeepSeek || isXAI || isAimlapi)) {2259 if (useLogprobs && (isOAI || isAzureOpenAI || isCustom || isDeepSeek || isXAI || isAimlapi)) {
2221 generate_data['logprobs'] = 5;2260 generate_data['logprobs'] = 5;
2222 }2261 }
22232262
2224 // Remove logit bias/logprobs/stop-strings if not supported by the model2263 // Remove logit bias/logprobs/stop-strings if not supported by the model
2225 const isVision = (m) => ['gpt', 'vision'].every(x => m.includes(x));2264 const isVision = (m) => ['gpt', 'vision'].every(x => m.includes(x));
2226 if (isOAI && isVision(oai_settings.openai_model) || isOpenRouter && isVision(oai_settings.openrouter_model)) {2265 if ((isOAI && isVision(oai_settings.openai_model)) || (isAzureOpenAI && isVision(oai_settings.azure_openai_model)) || (isOpenRouter && isVision(oai_settings.openrouter_model))) {
2227 delete generate_data.logit_bias;2266 delete generate_data.logit_bias;
2228 delete generate_data.stop;2267 delete generate_data.stop;
2229 delete generate_data.logprobs;2268 delete generate_data.logprobs;
2230 }2269 }
2231 if (isOAI && oai_settings.openai_model.includes('gpt-4.5') || isOpenRouter && oai_settings.openrouter_model.includes('gpt-4.5')) {2270 if ((isOAI && oai_settings.openai_model.includes('gpt-4.5')) || (isAzureOpenAI && oai_settings.azure_openai_model.includes('gpt-4.5')) || (isOpenRouter && oai_settings.openrouter_model.includes('gpt-4.5'))) {
2232 delete generate_data.logprobs;2271 delete generate_data.logprobs;
2233 }2272 }
22342273
@@ -2341,6 +2380,7 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } =
23412380
2342 const seedSupportedSources = [2381 const seedSupportedSources = [
2343 chat_completion_sources.OPENAI,2382 chat_completion_sources.OPENAI,
2383 chat_completion_sources.AZURE_OPENAI,
2344 chat_completion_sources.OPENROUTER,2384 chat_completion_sources.OPENROUTER,
2345 chat_completion_sources.MISTRALAI,2385 chat_completion_sources.MISTRALAI,
2346 chat_completion_sources.CUSTOM,2386 chat_completion_sources.CUSTOM,
@@ -2358,7 +2398,7 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } =
2358 generate_data['seed'] = oai_settings.seed;2398 generate_data['seed'] = oai_settings.seed;
2359 }2399 }
23602400
2361 if (isOAI && /^(o1|o3|o4)/.test(oai_settings.openai_model)) {2401 if ((isOAI && /^(o1|o3|o4)/.test(oai_settings.openai_model)) || (isAzureOpenAI && /^(o1|o3|o4)/.test(oai_settings.azure_openai_model))) {
2362 generate_data.max_completion_tokens = generate_data.max_tokens;2402 generate_data.max_completion_tokens = generate_data.max_tokens;
2363 delete generate_data.max_tokens;2403 delete generate_data.max_tokens;
2364 delete generate_data.logprobs;2404 delete generate_data.logprobs;
@@ -2381,7 +2421,7 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } =
2381 }2421 }
2382 }2422 }
23832423
2384 if (isOAI && /^gpt-5/.test(oai_settings.openai_model)) {2424 if ((isOAI && /^gpt-5/.test(oai_settings.openai_model)) || (isAzureOpenAI && /^gpt-5/.test(oai_settings.azure_openai_model))) {
2385 generate_data.max_completion_tokens = generate_data.max_tokens;2425 generate_data.max_completion_tokens = generate_data.max_tokens;
2386 delete generate_data.max_tokens;2426 delete generate_data.max_tokens;
2387 delete generate_data.logprobs;2427 delete generate_data.logprobs;
@@ -2551,6 +2591,7 @@ function parseChatCompletionLogprobs(data) {
25512591
2552 switch (oai_settings.chat_completion_source) {2592 switch (oai_settings.chat_completion_source) {
2553 case chat_completion_sources.OPENAI:2593 case chat_completion_sources.OPENAI:
2594 case chat_completion_sources.AZURE_OPENAI:
2554 case chat_completion_sources.DEEPSEEK:2595 case chat_completion_sources.DEEPSEEK:
2555 case chat_completion_sources.XAI:2596 case chat_completion_sources.XAI:
2556 case chat_completion_sources.CUSTOM:2597 case chat_completion_sources.CUSTOM:
@@ -2559,7 +2600,7 @@ function parseChatCompletionLogprobs(data) {
2559 }2600 }
2560 // OpenAI Text Completion API is treated as a chat completion source2601 // OpenAI Text Completion API is treated as a chat completion source
2561 // by SillyTavern, hence its presence in this function.2602 // by SillyTavern, hence its presence in this function.
2562 return textCompletionModels.includes(oai_settings.openai_model)2603 return textCompletionModels.includes(getChatCompletionModel())
2563 ? parseOpenAITextLogprobs(data.choices[0]?.logprobs)2604 ? parseOpenAITextLogprobs(data.choices[0]?.logprobs)
2564 : parseOpenAIChatLogprobs(data.choices[0]?.logprobs);2605 : parseOpenAIChatLogprobs(data.choices[0]?.logprobs);
2565 default:2606 default:
@@ -3497,6 +3538,10 @@ function loadOpenAISettings(data, settings) {
3497 oai_settings.custom_include_headers = settings.custom_include_headers ?? default_settings.custom_include_headers;3538 oai_settings.custom_include_headers = settings.custom_include_headers ?? default_settings.custom_include_headers;
3498 oai_settings.custom_prompt_post_processing = settings.custom_prompt_post_processing ?? default_settings.custom_prompt_post_processing;3539 oai_settings.custom_prompt_post_processing = settings.custom_prompt_post_processing ?? default_settings.custom_prompt_post_processing;
3499 oai_settings.google_model = settings.google_model ?? default_settings.google_model;3540 oai_settings.google_model = settings.google_model ?? default_settings.google_model;
3541 oai_settings.azure_base_url = settings.azure_base_url ?? default_settings.azure_base_url;
3542 oai_settings.azure_deployment_name = settings.azure_deployment_name ?? default_settings.azure_deployment_name;
3543 oai_settings.azure_api_version = settings.azure_api_version ?? default_settings.azure_api_version;
3544 oai_settings.azure_openai_model = settings.azure_openai_model ?? default_settings.azure_openai_model;
3500 oai_settings.vertexai_model = settings.vertexai_model ?? default_settings.vertexai_model;3545 oai_settings.vertexai_model = settings.vertexai_model ?? default_settings.vertexai_model;
3501 oai_settings.chat_completion_source = settings.chat_completion_source ?? default_settings.chat_completion_source;3546 oai_settings.chat_completion_source = settings.chat_completion_source ?? default_settings.chat_completion_source;
3502 oai_settings.show_external_models = settings.show_external_models ?? default_settings.show_external_models;3547 oai_settings.show_external_models = settings.show_external_models ?? default_settings.show_external_models;
@@ -3593,6 +3638,11 @@ function loadOpenAISettings(data, settings) {
3593 $(`#model_moonshot_select option[value="${oai_settings.moonshot_model}"`).prop('selected', true);3638 $(`#model_moonshot_select option[value="${oai_settings.moonshot_model}"`).prop('selected', true);
3594 $('#custom_model_id').val(oai_settings.custom_model);3639 $('#custom_model_id').val(oai_settings.custom_model);
3595 $('#custom_api_url_text').val(oai_settings.custom_url);3640 $('#custom_api_url_text').val(oai_settings.custom_url);
3641 $('#azure_base_url').val(oai_settings.azure_base_url);
3642 $('#azure_deployment_name').val(oai_settings.azure_deployment_name);
3643 $('#azure_api_version').val(oai_settings.azure_api_version);
3644 $('#azure_openai_model').val(oai_settings.azure_openai_model);
3645
3596 $('#openai_max_context').val(oai_settings.openai_max_context);3646 $('#openai_max_context').val(oai_settings.openai_max_context);
3597 $('#openai_max_context_counter').val(`${oai_settings.openai_max_context}`);3647 $('#openai_max_context_counter').val(`${oai_settings.openai_max_context}`);
3598 $('#model_openrouter_select').val(oai_settings.openrouter_model);3648 $('#model_openrouter_select').val(oai_settings.openrouter_model);
@@ -3771,6 +3821,12 @@ async function getStatusOpen() {
3771 return resultCheckStatus();3821 return resultCheckStatus();
3772 }3822 }
37733823
3824 if (oai_settings.chat_completion_source === chat_completion_sources.AZURE_OPENAI && !isValidUrl(oai_settings.azure_base_url)) {
3825 console.debug('Invalid endpoint URL of Azure OpenAI API:', oai_settings.azure_base_url);
3826 setOnlineStatus(t`Invalid Azure endpoint URL. Requests may fail.`);
3827 return resultCheckStatus();
3828 }
3829
3774 let data = {3830 let data = {
3775 reverse_proxy: oai_settings.reverse_proxy,3831 reverse_proxy: oai_settings.reverse_proxy,
3776 proxy_password: oai_settings.proxy_password,3832 proxy_password: oai_settings.proxy_password,
@@ -3796,6 +3852,12 @@ async function getStatusOpen() {
3796 data.custom_include_headers = oai_settings.custom_include_headers;3852 data.custom_include_headers = oai_settings.custom_include_headers;
3797 }3853 }
37983854
3855 if (oai_settings.chat_completion_source === chat_completion_sources.AZURE_OPENAI) {
3856 data.azure_base_url = oai_settings.azure_base_url;
3857 data.azure_deployment_name = oai_settings.azure_deployment_name;
3858 data.azure_api_version = oai_settings.azure_api_version;
3859 }
3860
3799 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;3861 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;
3800 if (canBypass) {3862 if (canBypass) {
3801 setOnlineStatus(t`Status check bypassed`);3863 setOnlineStatus(t`Status check bypassed`);
@@ -3877,6 +3939,10 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
3877 custom_prompt_post_processing: settings.custom_prompt_post_processing,3939 custom_prompt_post_processing: settings.custom_prompt_post_processing,
3878 google_model: settings.google_model,3940 google_model: settings.google_model,
3879 vertexai_model: settings.vertexai_model,3941 vertexai_model: settings.vertexai_model,
3942 azure_base_url: settings.azure_base_url,
3943 azure_deployment_name: settings.azure_deployment_name,
3944 azure_api_version: settings.azure_api_version,
3945 azure_openai_model: settings.azure_openai_model,
3880 temperature: settings.temp_openai,3946 temperature: settings.temp_openai,
3881 frequency_penalty: settings.freq_pen_openai,3947 frequency_penalty: settings.freq_pen_openai,
3882 presence_penalty: settings.pres_pen_openai,3948 presence_penalty: settings.pres_pen_openai,
@@ -4839,6 +4905,14 @@ async function onModelChange() {
4839 oai_settings.cometapi_model = value;4905 oai_settings.cometapi_model = value;
4840 }4906 }
48414907
4908 if ($(this).is('#azure_openai_model')) {
4909 if (!value) {
4910 console.debug('Null Azure OpenAI model selected. Ignoring.');
4911 return;
4912 }
4913 oai_settings.azure_openai_model = value;
4914 }
4915
4842 if ([chat_completion_sources.MAKERSUITE, chat_completion_sources.VERTEXAI].includes(oai_settings.chat_completion_source)) {4916 if ([chat_completion_sources.MAKERSUITE, chat_completion_sources.VERTEXAI].includes(oai_settings.chat_completion_source)) {
4843 if (oai_settings.max_context_unlocked) {4917 if (oai_settings.max_context_unlocked) {
4844 $('#openai_max_context').attr('max', max_2mil);4918 $('#openai_max_context').attr('max', max_2mil);
@@ -4913,7 +4987,7 @@ async function onModelChange() {
4913 $('#temp_openai').attr('max', claude_max_temp).val(oai_settings.temp_openai).trigger('input');4987 $('#temp_openai').attr('max', claude_max_temp).val(oai_settings.temp_openai).trigger('input');
4914 }4988 }
49154989
4916 if (oai_settings.chat_completion_source == chat_completion_sources.OPENAI) {4990 if ([chat_completion_sources.AZURE_OPENAI, chat_completion_sources.OPENAI].includes(oai_settings.chat_completion_source)) {
4917 $('#openai_max_context').attr('max', getMaxContextOpenAI(value));4991 $('#openai_max_context').attr('max', getMaxContextOpenAI(value));
4918 oai_settings.openai_max_context = Math.min(oai_settings.openai_max_context, Number($('#openai_max_context').attr('max')));4992 oai_settings.openai_max_context = Math.min(oai_settings.openai_max_context, Number($('#openai_max_context').attr('max')));
4919 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');4993 $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
@@ -5419,6 +5493,20 @@ async function onConnectButtonClick(e) {
5419 }5493 }
5420 }5494 }
54215495
5496 if (oai_settings.chat_completion_source == chat_completion_sources.AZURE_OPENAI) {
5497 const api_key_azure_openai = String($('#api_key_azure_openai').val()).trim();
5498
5499 if (api_key_azure_openai.length) {
5500 await writeSecret(SECRET_KEYS.AZURE_OPENAI, api_key_azure_openai);
5501 }
5502
5503 if (!api_key_azure_openai && !secret_state[SECRET_KEYS.AZURE_OPENAI]) {
5504 console.log('No secret key saved for Azure OpenAI');
5505 return;
5506 }
5507 }
5508
5509
5422 startStatusLoading();5510 startStatusLoading();
5423 saveSettingsDebounced();5511 saveSettingsDebounced();
5424 await getStatusOpen();5512 await getStatusOpen();
@@ -5492,6 +5580,9 @@ function toggleChatCompletionForms() {
5492 else if (oai_settings.chat_completion_source == chat_completion_sources.COMETAPI) {5580 else if (oai_settings.chat_completion_source == chat_completion_sources.COMETAPI) {
5493 $('#model_cometapi_select').trigger('change');5581 $('#model_cometapi_select').trigger('change');
5494 }5582 }
5583 else if (oai_settings.chat_completion_source == chat_completion_sources.AZURE_OPENAI) {
5584 $('#azure_openai_model').trigger('change');
5585 }
54955586
5496 $('[data-source]').each(function () {5587 $('[data-source]').each(function () {
5497 const validSources = $(this).data('source').split(',');5588 const validSources = $(this).data('source').split(',');
@@ -5611,10 +5702,15 @@ export function isImageInliningSupported() {
56115702
5612 switch (oai_settings.chat_completion_source) {5703 switch (oai_settings.chat_completion_source) {
5613 case chat_completion_sources.OPENAI:5704 case chat_completion_sources.OPENAI:
5705 case chat_completion_sources.AZURE_OPENAI: {
5706 const modelToCheck = oai_settings.chat_completion_source === chat_completion_sources.AZURE_OPENAI
5707 ? oai_settings.azure_openai_model
5708 : oai_settings.openai_model;
5614 return visionSupportedModels.some(model =>5709 return visionSupportedModels.some(model =>
5615 oai_settings.openai_model.includes(model)5710 modelToCheck.includes(model)
5616 && ['gpt-4-turbo-preview', 'o1-mini', 'o3-mini'].some(x => !oai_settings.openai_model.includes(x)),5711 && ['gpt-4-turbo-preview', 'o1-mini', 'o3-mini'].some(x => !modelToCheck.includes(x)),
5617 );5712 );
5713 }
5618 case chat_completion_sources.MAKERSUITE:5714 case chat_completion_sources.MAKERSUITE:
5619 return visionSupportedModels.some(model => oai_settings.google_model.includes(model));5715 return visionSupportedModels.some(model => oai_settings.google_model.includes(model));
5620 case chat_completion_sources.VERTEXAI:5716 case chat_completion_sources.VERTEXAI:
@@ -6295,6 +6391,21 @@ export function initOpenAI() {
6295 saveSettingsDebounced();6391 saveSettingsDebounced();
6296 });6392 });
62976393
6394 $('#azure_base_url').on('input', function () {
6395 oai_settings.azure_base_url = String($(this).val());
6396 saveSettingsDebounced();
6397 });
6398
6399 $('#azure_deployment_name').on('input', function () {
6400 oai_settings.azure_deployment_name = String($(this).val());
6401 saveSettingsDebounced();
6402 });
6403
6404 $('#azure_api_version').on('input change', function () {
6405 oai_settings.azure_api_version = String($(this).val());
6406 saveSettingsDebounced();
6407 });
6408
6298 $('#character_names_none').on('input', function () {6409 $('#character_names_none').on('input', function () {
6299 oai_settings.names_behavior = character_names_behavior.NONE;6410 oai_settings.names_behavior = character_names_behavior.NONE;
6300 setNamesBehaviorControls();6411 setNamesBehaviorControls();
@@ -6453,6 +6564,7 @@ export function initOpenAI() {
6453 $('#model_cometapi_select').on('change', onModelChange);6564 $('#model_cometapi_select').on('change', onModelChange);
6454 $('#model_moonshot_select').on('change', onModelChange);6565 $('#model_moonshot_select').on('change', onModelChange);
6455 $('#model_fireworks_select').on('change', onModelChange);6566 $('#model_fireworks_select').on('change', onModelChange);
6567 $('#azure_openai_model').on('change', onModelChange);
6456 $('#settings_preset_openai').on('change', onSettingsPresetChange);6568 $('#settings_preset_openai').on('change', onSettingsPresetChange);
6457 $('#new_oai_preset').on('click', onNewPresetClick);6569 $('#new_oai_preset').on('click', onNewPresetClick);
6458 $('#delete_oai_preset').on('click', onDeletePresetClick);6570 $('#delete_oai_preset').on('click', onDeletePresetClick);
public/scripts/secrets.js+3 -0
@@ -47,6 +47,7 @@ export const SECRET_KEYS = {
47 PERPLEXITY: 'api_key_perplexity',47 PERPLEXITY: 'api_key_perplexity',
48 GROQ: 'api_key_groq',48 GROQ: 'api_key_groq',
49 AZURE_TTS: 'api_key_azure_tts',49 AZURE_TTS: 'api_key_azure_tts',
50 AZURE_OPENAI: 'api_key_azure_openai',
50 FEATHERLESS: 'api_key_featherless',51 FEATHERLESS: 'api_key_featherless',
51 HUGGINGFACE: 'api_key_huggingface',52 HUGGINGFACE: 'api_key_huggingface',
52 STABILITY: 'api_key_stability',53 STABILITY: 'api_key_stability',
@@ -122,6 +123,7 @@ const FRIENDLY_NAMES = {
122 [SECRET_KEYS.MINIMAX_GROUP_ID]: 'MiniMax Group ID',123 [SECRET_KEYS.MINIMAX_GROUP_ID]: 'MiniMax Group ID',
123 [SECRET_KEYS.MOONSHOT]: 'Moonshot AI',124 [SECRET_KEYS.MOONSHOT]: 'Moonshot AI',
124 [SECRET_KEYS.COMETAPI]: 'CometAPI',125 [SECRET_KEYS.COMETAPI]: 'CometAPI',
126 [SECRET_KEYS.AZURE_OPENAI]: 'Azure OpenAI',
125};127};
126128
127const INPUT_MAP = {129const INPUT_MAP = {
@@ -160,6 +162,7 @@ const INPUT_MAP = {
160 [SECRET_KEYS.MOONSHOT]: '#api_key_moonshot',162 [SECRET_KEYS.MOONSHOT]: '#api_key_moonshot',
161 [SECRET_KEYS.FIREWORKS]: '#api_key_fireworks',163 [SECRET_KEYS.FIREWORKS]: '#api_key_fireworks',
162 [SECRET_KEYS.COMETAPI]: '#api_key_cometapi',164 [SECRET_KEYS.COMETAPI]: '#api_key_cometapi',
165 [SECRET_KEYS.AZURE_OPENAI]: '#api_key_azure_openai',
163};166};
164167
165const getLabel = () => moment().format('L LT');168const getLabel = () => moment().format('L LT');
public/scripts/tokenizers.js+4 -0
@@ -586,6 +586,10 @@ export function getTokenizerModel() {
586 const nemoTokenizer = 'nemo';586 const nemoTokenizer = 'nemo';
587 const deepseekTokenizer = 'deepseek';587 const deepseekTokenizer = 'deepseek';
588588
589 if (oai_settings.chat_completion_source == chat_completion_sources.AZURE_OPENAI) {
590 return oai_settings.azure_openai_model || turboTokenizer;
591 }
592
589 if (oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK) {593 if (oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK) {
590 return deepseekTokenizer;594 return deepseekTokenizer;
591 }595 }
public/scripts/tool-calling.js+1 -0
@@ -659,6 +659,7 @@ export class ToolManager {
659 chat_completion_sources.FIREWORKS,659 chat_completion_sources.FIREWORKS,
660 chat_completion_sources.COMETAPI,660 chat_completion_sources.COMETAPI,
661 chat_completion_sources.ELECTRONHUB,661 chat_completion_sources.ELECTRONHUB,
662 chat_completion_sources.AZURE_OPENAI,
662 ];663 ];
663 return supportedSources.includes(oai_settings.chat_completion_source);664 return supportedSources.includes(oai_settings.chat_completion_source);
664 }665 }
src/constants.js+40 -0
@@ -182,6 +182,7 @@ export const CHAT_COMPLETION_SOURCES = {
182 MOONSHOT: 'moonshot',182 MOONSHOT: 'moonshot',
183 FIREWORKS: 'fireworks',183 FIREWORKS: 'fireworks',
184 COMETAPI: 'cometapi',184 COMETAPI: 'cometapi',
185 AZURE_OPENAI: 'azure_openai',
185};186};
186187
187/**188/**
@@ -408,6 +409,45 @@ export const VLLM_KEYS = [
408 'guided_whitespace_pattern',409 'guided_whitespace_pattern',
409];410];
410411
412export const AZURE_OPENAI_KEYS = [
413 'messages',
414 'temperature',
415 'frequency_penalty',
416 'presence_penalty',
417 'top_p',
418 'max_tokens',
419 'max_completion_tokens',
420 'stream',
421 'logit_bias',
422 'stop',
423 'n',
424 'logprobs',
425 'seed',
426 'tools',
427 'tool_choice',
428 'reasoning_effort',
429];
430
431export const OPENAI_REASONING_EFFORT_MODELS = [
432 'o1',
433 'o3-mini',
434 'o3-mini-2025-01-31',
435 'o4-mini',
436 'o4-mini-2025-04-16',
437 'o3',
438 'o3-2025-04-16',
439 'gpt-5',
440 'gpt-5-2025-08-07',
441 'gpt-5-mini',
442 'gpt-5-mini-2025-08-07',
443 'gpt-5-nano',
444 'gpt-5-nano-2025-08-07',
445];
446
447export const OPENAI_REASONING_EFFORT_MAP = {
448 min: 'minimal',
449};
450
411export const LOG_LEVELS = {451export const LOG_LEVELS = {
412 DEBUG: 0,452 DEBUG: 0,
413 INFO: 1,453 INFO: 1,
src/endpoints/backends/chat-completions.js+178 -20
@@ -6,8 +6,11 @@ import urlJoin from 'url-join';
66
7import {7import {
8 AIMLAPI_HEADERS,8 AIMLAPI_HEADERS,
9 AZURE_OPENAI_KEYS,
9 CHAT_COMPLETION_SOURCES,10 CHAT_COMPLETION_SOURCES,
10 GEMINI_SAFETY,11 GEMINI_SAFETY,
12 OPENAI_REASONING_EFFORT_MAP,
13 OPENAI_REASONING_EFFORT_MODELS,
11 OPENROUTER_HEADERS,14 OPENROUTER_HEADERS,
12} from '../../constants.js';15} from '../../constants.js';
13import {16import {
@@ -1294,6 +1297,100 @@ async function sendElectronHubRequest(request, response) {
1294 }1297 }
1295}1298}
12961299
1300/**
1301 * Sends a chat completion request to Azure OpenAI.
1302 * @param {express.Request} request Express request object (contains request.body with all generate_data)
1303 * @param {express.Response} response Express response object
1304 */
1305async function sendAzureOpenAIRequest(request, response) {
1306 // 1. GATHER & VALIDATE SETTINGS
1307 const { azure_base_url, azure_deployment_name, azure_api_version } = request.body;
1308 const apiKey = readSecret(request.user.directories, SECRET_KEYS.AZURE_OPENAI);
1309 if (!azure_base_url || !azure_deployment_name || !azure_api_version || !apiKey) {
1310 return response.status(400).send({
1311 error: {
1312 message: 'Azure OpenAI configuration is incomplete. Please provide Base URL, Deployment Name, API Version, and API Key in the connection settings.',
1313 },
1314 });
1315 }
1316
1317 // 2. PREPARE THE REQUEST
1318 const url = new URL(`/openai/deployments/${azure_deployment_name}/chat/completions`, azure_base_url);
1319 url.searchParams.set('api-version', azure_api_version);
1320 const endpointUrl = url.toString();
1321
1322 // Create the base payload with all standard parameters
1323 const apiRequestBody = /** @type {any} */ ({});
1324 for (const key of AZURE_OPENAI_KEYS) {
1325 if (Object.hasOwn(request.body, key)) {
1326 apiRequestBody[key] = request.body[key];
1327 }
1328 }
1329
1330 // Handle Structured Output (JSON Mode) by translating the custom `json_schema` object.
1331 if (request.body.json_schema) {
1332 apiRequestBody['response_format'] = {
1333 type: 'json_schema',
1334 json_schema: {
1335 name: request.body.json_schema.name,
1336 strict: request.body.json_schema.strict ?? true,
1337 schema: request.body.json_schema.value,
1338 },
1339 };
1340 }
1341
1342 // Adjust logprobs for Azure OpenAI, which follows the OpenAI Chat Completions API spec.
1343 if (typeof apiRequestBody.logprobs === 'number' && apiRequestBody.logprobs > 0) {
1344 apiRequestBody.top_logprobs = apiRequestBody.logprobs;
1345 apiRequestBody.logprobs = true;
1346 }
1347
1348 // Do not send reasoning effort to models which do not support it
1349 apiRequestBody['reasoning_effort'] = OPENAI_REASONING_EFFORT_MODELS.includes(request.body.model)
1350 ? OPENAI_REASONING_EFFORT_MAP[request.body.reasoning_effort] ?? request.body.reasoning_effort
1351 : undefined;
1352
1353 const controller = new AbortController();
1354 request.socket.removeAllListeners('close');
1355 request.socket.on('close', () => controller.abort());
1356
1357 const config = {
1358 method: 'POST',
1359 headers: {
1360 'Content-Type': 'application/json',
1361 'api-key': apiKey,
1362 },
1363 body: JSON.stringify(apiRequestBody),
1364 signal: controller.signal,
1365 };
1366
1367 console.info(`Sending request to Azure OpenAI: ${endpointUrl}`);
1368 console.debug('Azure OpenAI Request Body:', apiRequestBody);
1369 try {
1370 const fetchResponse = await fetch(endpointUrl, config);
1371
1372 if (request.body.stream) {
1373 return forwardFetchResponse(fetchResponse, response);
1374 }
1375
1376 if (fetchResponse.ok) {
1377 /** @type {any} */
1378 const json = await fetchResponse.json();
1379 console.debug('Azure OpenAI response:', json);
1380 return response.send(json);
1381 }
1382
1383 const text = await fetchResponse.text();
1384 const data = tryParse(text) || { error: { message: fetchResponse.statusText || 'Unknown error occurred' } };
1385 return response.status(500).send(data);
1386 } catch (error) {
1387 const message = error.name === 'AbortError'
1388 ? 'Request was aborted by the client.'
1389 : (error.message || 'An unknown network error occurred.');
1390 return response.status(500).send({ error: { message, ...error } });
1391 }
1392}
1393
1297export const router = express.Router();1394export const router = express.Router();
12981395
1299router.post('/status', async function (request, statusResponse) {1396router.post('/status', async function (request, statusResponse) {
@@ -1404,6 +1501,84 @@ router.post('/status', async function (request, statusResponse) {
1404 console.error('Error fetching Google AI Studio models:', error);1501 console.error('Error fetching Google AI Studio models:', error);
1405 return statusResponse.send({ error: true, bypass: true, data: { data: [] } });1502 return statusResponse.send({ error: true, bypass: true, data: { data: [] } });
1406 }1503 }
1504 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.AZURE_OPENAI) {
1505 const { azure_base_url, azure_deployment_name, azure_api_version } = request.body;
1506 const apiKey = readSecret(request.user.directories, SECRET_KEYS.AZURE_OPENAI);
1507
1508 // 1) Validate configuration from the frontend
1509 if (!apiKey || !azure_base_url || !azure_deployment_name || !azure_api_version) {
1510 console.warn('Azure OpenAI status check failed: missing config from frontend.');
1511 return statusResponse.status(400).send({ error: true, message: 'Azure configuration is incomplete.' });
1512 }
1513 // 2) Build URLs using the URL API for consistency and robustness.
1514 const modelsUrl = new URL('/openai/models', azure_base_url);
1515 modelsUrl.searchParams.set('api-version', azure_api_version);
1516
1517 const chatUrl = new URL(`/openai/deployments/${azure_deployment_name}/chat/completions`, azure_base_url);
1518 chatUrl.searchParams.set('api-version', azure_api_version);
1519
1520 // Map common status codes to user-friendly error messages
1521 const azureStatusErrorMap = {
1522 400: 'API version may be invalid for this resource.',
1523 401: 'Invalid API key or insufficient permissions.',
1524 403: 'Invalid API key or insufficient permissions.',
1525 404: 'Endpoint URL appears incorrect (404).',
1526 };
1527
1528 try {
1529 // ---- A) GET /models: fast sanity check for endpoint + api key + api version ----
1530 const apiConfigTest = await fetch(modelsUrl, {
1531 method: 'GET',
1532 headers: { 'api-key': apiKey, 'Accept': 'application/json' },
1533 });
1534
1535 if (!apiConfigTest.ok) {
1536 let errText = '';
1537 try { errText = await apiConfigTest.text(); } catch { /* response body may be empty */ }
1538
1539 console.warn('Azure OpenAI GET /models failed:', apiConfigTest.status, apiConfigTest.statusText, errText || '');
1540
1541 const defaultMessage = `Azure Models endpoint error: ${apiConfigTest.statusText}`;
1542 const message = azureStatusErrorMap[apiConfigTest.status] ?? defaultMessage;
1543 return statusResponse.status(apiConfigTest.status).send({ error: true, message });
1544 }
1545
1546 // ---- B) POST /chat/completions: verify deployment + read underlying model ID ----
1547 // Small, deterministic probe to minimize cost/latency
1548 const modelPayload = {
1549 messages: [{ role: 'user', content: 'Say word Hi' }],
1550 stream: false,
1551 max_completion_tokens: 5,
1552 };
1553
1554 const modelRequest = await fetch(chatUrl, {
1555 method: 'POST',
1556 headers: { 'api-key': apiKey, 'Content-Type': 'application/json', 'Accept': 'application/json' },
1557 body: JSON.stringify(modelPayload),
1558 });
1559
1560 let modelResponse;
1561 try {
1562 modelResponse = await modelRequest.json();
1563 } catch {
1564 modelResponse = { raw: 'Failed to parse JSON response from chat completions probe.' };
1565 }
1566
1567 const modelId = /** @type {any} */ (modelResponse)?.model;
1568 if (!modelId) {
1569 console.warn('Azure status check succeeded but could not find a model ID in the response.');
1570 console.debug('Azure Response Body:', modelResponse);
1571 // Keep a benign success to avoid UX disruption in the UI
1572 return statusResponse.send({ data: [] });
1573 }
1574
1575 console.info(color.green('Azure OpenAI connection successful. Detected model:'), modelId);
1576 // Consistent response format: always an array of { id }
1577 return statusResponse.send({ data: [{ id: modelId }] });
1578 } catch (error) {
1579 console.error('Azure OpenAI status check connection error:', error);
1580 return statusResponse.status(500).send({ error: true, message: 'Failed to connect to the Azure endpoint.' });
1581 }
1407 } else {1582 } else {
1408 console.warn('This chat completion source is not supported yet.');1583 console.warn('This chat completion source is not supported yet.');
1409 return statusResponse.status(400).send({ error: true });1584 return statusResponse.status(400).send({ error: true });
@@ -1596,6 +1771,7 @@ router.post('/generate', function (request, response) {
1596 case CHAT_COMPLETION_SOURCES.AIMLAPI: return sendAimlapiRequest(request, response);1771 case CHAT_COMPLETION_SOURCES.AIMLAPI: return sendAimlapiRequest(request, response);
1597 case CHAT_COMPLETION_SOURCES.XAI: return sendXaiRequest(request, response);1772 case CHAT_COMPLETION_SOURCES.XAI: return sendXaiRequest(request, response);
1598 case CHAT_COMPLETION_SOURCES.ELECTRONHUB: return sendElectronHubRequest(request, response);1773 case CHAT_COMPLETION_SOURCES.ELECTRONHUB: return sendElectronHubRequest(request, response);
1774 case CHAT_COMPLETION_SOURCES.AZURE_OPENAI: return sendAzureOpenAIRequest(request, response);
1599 }1775 }
16001776
1601 let apiUrl;1777 let apiUrl;
@@ -1803,26 +1979,8 @@ router.post('/generate', function (request, response) {
18031979
1804 // A few of OpenAIs reasoning models support reasoning effort1980 // A few of OpenAIs reasoning models support reasoning effort
1805 if (request.body.reasoning_effort && [CHAT_COMPLETION_SOURCES.CUSTOM, CHAT_COMPLETION_SOURCES.OPENAI].includes(request.body.chat_completion_source)) {1981 if (request.body.reasoning_effort && [CHAT_COMPLETION_SOURCES.CUSTOM, CHAT_COMPLETION_SOURCES.OPENAI].includes(request.body.chat_completion_source)) {
1806 const reasoningEffortModels = [1982 if (OPENAI_REASONING_EFFORT_MODELS.includes(request.body.model)) {
1807 'o1',1983 bodyParams['reasoning_effort'] = OPENAI_REASONING_EFFORT_MAP[request.body.reasoning_effort] ?? request.body.reasoning_effort;
1808 'o3-mini',
1809 'o3-mini-2025-01-31',
1810 'o4-mini',
1811 'o4-mini-2025-04-16',
1812 'o3',
1813 'o3-2025-04-16',
1814 'gpt-5',
1815 'gpt-5-2025-08-07',
1816 'gpt-5-mini',
1817 'gpt-5-mini-2025-08-07',
1818 'gpt-5-nano',
1819 'gpt-5-nano-2025-08-07',
1820 ];
1821 const reasoningEffortMap = {
1822 min: 'minimal',
1823 };
1824 if (reasoningEffortModels.includes(request.body.model)) {
1825 bodyParams['reasoning_effort'] = reasoningEffortMap[request.body.reasoning_effort] ?? request.body.reasoning_effort;
1826 }1984 }
1827 }1985 }
18281986
src/endpoints/secrets.js+1 -0
@@ -60,6 +60,7 @@ export const SECRET_KEYS = {
60 MINIMAX_GROUP_ID: 'minimax_group_id',60 MINIMAX_GROUP_ID: 'minimax_group_id',
61 MOONSHOT: 'api_key_moonshot',61 MOONSHOT: 'api_key_moonshot',
62 COMETAPI: 'api_key_cometapi',62 COMETAPI: 'api_key_cometapi',
63 AZURE_OPENAI: 'api_key_azure_openai',
63};64};
6465
65/**66/**