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>
This commit is contained in:
@@ -0,0 +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>
|
||||
|
After Width: | Height: | Size: 468 B |
+57
-13
@@ -652,7 +652,7 @@
|
||||
<input type="number" id="openai_max_tokens" name="openai_max_tokens" class="text_pole" min="1" max="65536">
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block" data-source="openai,custom,xai,aimlapi,moonshot">
|
||||
<div class="range-block" data-source="openai,custom,xai,aimlapi,moonshot,azure_openai">
|
||||
<div class="range-block-title" data-i18n="Multiple swipes per generation">
|
||||
Multiple swipes per generation
|
||||
</div>
|
||||
@@ -691,7 +691,7 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<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">
|
||||
<div class="range-block-title" data-i18n="Temperature">
|
||||
Temperature
|
||||
</div>
|
||||
@@ -704,7 +704,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block" data-source="openai,aimlapi,openrouter,custom,cohere,perplexity,groq,mistralai,electronhub,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi">
|
||||
<div class="range-block" data-source="openai,aimlapi,openrouter,custom,cohere,perplexity,groq,mistralai,electronhub,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai">
|
||||
<div class="range-block-title" data-i18n="Frequency Penalty">
|
||||
Frequency Penalty
|
||||
</div>
|
||||
@@ -717,7 +717,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block" data-source="openai,aimlapi,openrouter,custom,cohere,perplexity,groq,mistralai,electronhub,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi">
|
||||
<div class="range-block" data-source="openai,aimlapi,openrouter,custom,cohere,perplexity,groq,mistralai,electronhub,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai">
|
||||
<div class="range-block-title" data-i18n="Presence Penalty">
|
||||
Presence Penalty
|
||||
</div>
|
||||
@@ -743,7 +743,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<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">
|
||||
<div class="range-block-title" data-i18n="Top P">
|
||||
Top P
|
||||
</div>
|
||||
@@ -980,7 +980,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block" data-source="openai,openrouter,mistralai,custom,cohere,groq,electronhub,nanogpt,xai,pollinations,aimlapi,makersuite,vertexai">
|
||||
<div class="range-block" data-source="openai,openrouter,mistralai,custom,cohere,groq,electronhub,nanogpt,xai,pollinations,aimlapi,makersuite,vertexai,azure_openai">
|
||||
<div class="range-block-title justifyLeft" data-i18n="Seed">
|
||||
Seed
|
||||
</div>
|
||||
@@ -1984,7 +1984,7 @@
|
||||
</b>
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block" data-source="openai,cohere,mistralai,custom,claude,aimlapi,openrouter,groq,deepseek,makersuite,vertexai,ai21,xai,pollinations,moonshot,fireworks,cometapi,electronhub">
|
||||
<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">
|
||||
<label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">
|
||||
<input id="openai_function_calling" type="checkbox" />
|
||||
<span data-i18n="Enable function calling">Enable function calling</span>
|
||||
@@ -1999,7 +1999,7 @@
|
||||
<strong data-i18n="enable_functions_desc_4">Not supported when Prompt Post-Processing with "no tools" is used!</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block" data-source="openai,aimlapi,openrouter,mistralai,makersuite,vertexai,claude,custom,xai,pollinations,moonshot,cohere,cometapi,nanogpt,electronhub">
|
||||
<div class="range-block" data-source="openai,aimlapi,openrouter,mistralai,makersuite,vertexai,claude,custom,xai,pollinations,moonshot,cohere,cometapi,nanogpt,electronhub,azure_openai">
|
||||
<label for="openai_image_inlining" class="checkbox_label flexWrap widthFreeExpand">
|
||||
<input id="openai_image_inlining" type="checkbox" />
|
||||
<span data-i18n="Send inline images">Send inline images</span>
|
||||
@@ -2015,7 +2015,7 @@
|
||||
<code><i class="fa-solid fa-wand-magic-sparkles"></i></code>
|
||||
<span data-i18n="image_inlining_hint_3">menu to attach an image file to the chat.</span>
|
||||
</div>
|
||||
<div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,xai,pollinations,cohere,cometapi,nanogpt,moonshot,aimlapi,openrouter,mistralai,electronhub">
|
||||
<div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,xai,pollinations,cohere,cometapi,nanogpt,moonshot,aimlapi,openrouter,mistralai,electronhub,azure_openai">
|
||||
<div class="flex-container oneline-dropdown">
|
||||
<label for="openai_inline_image_quality" data-i18n="Inline Image Quality">
|
||||
Inline Image Quality
|
||||
@@ -2077,7 +2077,7 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block" data-source="deepseek,aimlapi,openrouter,custom,claude,xai,makersuite,vertexai,pollinations,moonshot,mistralai,fireworks,cometapi,electronhub">
|
||||
<div class="range-block" data-source="deepseek,aimlapi,openrouter,custom,claude,xai,makersuite,vertexai,pollinations,moonshot,mistralai,fireworks,cometapi,electronhub,azure_openai">
|
||||
<label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">
|
||||
<input id="openai_show_thoughts" type="checkbox" />
|
||||
<span data-i18n="Request model reasoning">Request model reasoning</span>
|
||||
@@ -2091,7 +2091,7 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,claude,xai,makersuite,vertexai,aimlapi,openrouter,pollinations,perplexity,cometapi,electronhub">
|
||||
<div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,claude,xai,makersuite,vertexai,aimlapi,openrouter,pollinations,perplexity,cometapi,electronhub,azure_openai">
|
||||
<div class="flex-container oneline-dropdown" title="Constrains effort on reasoning for reasoning models. 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.">
|
||||
<label for="openai_reasoning_effort">
|
||||
<span data-i18n="Reasoning Effort">Reasoning Effort</span>
|
||||
@@ -2105,7 +2105,7 @@
|
||||
<option data-i18n="openai_reasoning_effort_high" value="high">High</option>
|
||||
<option data-i18n="openai_reasoning_effort_maximum" value="max">Maximum</option>
|
||||
</select>
|
||||
<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.">
|
||||
<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.">
|
||||
OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level.
|
||||
</div>
|
||||
<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 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block m-t-1" data-source="openai,aimlapi,openrouter,custom">
|
||||
<div class="range-block m-t-1" data-source="openai,aimlapi,openrouter,custom,azure_openai">
|
||||
<div id="logit_bias_openai" class="range-block-title openai_restorable" data-i18n="Logit Bias">
|
||||
Logit Bias
|
||||
</div>
|
||||
@@ -2802,6 +2802,7 @@
|
||||
<optgroup>
|
||||
<option value="ai21">AI21</option>
|
||||
<option value="aimlapi">AI/ML API</option>
|
||||
<option value="azure_openai">Azure OpenAI</option>
|
||||
<option value="claude">Claude</option>
|
||||
<option value="cohere">Cohere</option>
|
||||
<!-- Temporarily disabled. -->
|
||||
@@ -3735,6 +3736,49 @@
|
||||
<option value="kimi-thinking-preview">kimi-thinking-preview</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="azure_openai_settings" data-source="azure_openai">
|
||||
<!-- Azure Base URL -->
|
||||
<h4><span data-i18n="Azure Base URL">Azure Base URL</span></h4>
|
||||
<div class="flex-container">
|
||||
<input id="azure_base_url" data-setting="azure_base_url" class="text_pole wide100p" type="text" placeholder="https://your-resource.openai.azure.com/">
|
||||
</div>
|
||||
|
||||
<!-- Azure Deployment Name -->
|
||||
<h4><span data-i18n="Deployment Name">Deployment Name</span></h4>
|
||||
<div class="flex-container">
|
||||
<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.">
|
||||
</div>
|
||||
|
||||
<!-- Azure API Version Dropdown -->
|
||||
<h4><span data-i18n="API Version">API Version</span></h4>
|
||||
<div class="flex-container">
|
||||
<select id="azure_api_version" data-setting="azure_api_version" class="text_pole wide100p">
|
||||
<option value="2025-04-01-preview">2025-04-01-preview</option>
|
||||
<option value="2024-10-21">2024-10-21</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Azure API Key -->
|
||||
<h4><span data-i18n="Azure API Key">Azure API Key</span></h4>
|
||||
<div class="flex-container">
|
||||
<input id="api_key_azure_openai" data-setting="api_key_azure_openai" class="text_pole flex1" type="password" autocomplete="off">
|
||||
<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>
|
||||
</div>
|
||||
<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">
|
||||
For privacy reasons, your API key will be hidden after you click 'Connect'.
|
||||
</div>
|
||||
|
||||
<!-- Model Name (Select) -->
|
||||
<h4><span data-i18n="Model Name">Model Name</span></h4>
|
||||
<div class="flex-container">
|
||||
<select id="azure_openai_model" data-setting="azure_openai_model" class="text_pole wide100p">
|
||||
<option value="" disabled selected data-i18n="Click 'Connect' to fetch model name">Click 'Connect' to fetch model name</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div id="prompt_post_processing_form">
|
||||
<h4>
|
||||
<span data-i18n="Prompt Post-Processing">
|
||||
|
||||
@@ -5370,6 +5370,7 @@ export function extractJsonFromData(data, { mainApi = null, chatCompletionSource
|
||||
case chat_completion_sources.COHERE:
|
||||
case chat_completion_sources.XAI:
|
||||
case chat_completion_sources.ELECTRONHUB:
|
||||
case chat_completion_sources.AZURE_OPENAI:
|
||||
default:
|
||||
result = tryParse(text);
|
||||
break;
|
||||
|
||||
@@ -412,6 +412,7 @@ function RA_autoconnect(PrevApi) {
|
||||
|| (secret_state[SECRET_KEYS.COMETAPI] && oai_settings.chat_completion_source == chat_completion_sources.COMETAPI)
|
||||
|| (oai_settings.chat_completion_source === chat_completion_sources.POLLINATIONS)
|
||||
|| (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM)
|
||||
|| (secret_state[SECRET_KEYS.AZURE_OPENAI] && oai_settings.chat_completion_source == chat_completion_sources.AZURE_OPENAI)
|
||||
) {
|
||||
$('#api_button_openai').trigger('click');
|
||||
}
|
||||
|
||||
+125
-13
@@ -188,6 +188,7 @@ export const chat_completion_sources = {
|
||||
MOONSHOT: 'moonshot',
|
||||
FIREWORKS: 'fireworks',
|
||||
COMETAPI: 'cometapi',
|
||||
AZURE_OPENAI: 'azure_openai',
|
||||
};
|
||||
|
||||
const character_names_behavior = {
|
||||
@@ -241,6 +242,8 @@ const sensitiveFields = [
|
||||
'custom_include_headers',
|
||||
'vertexai_region',
|
||||
'vertexai_express_project_id',
|
||||
'azure_base_url',
|
||||
'azure_deployment_name',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -331,6 +334,10 @@ export const settingsToUpdate = {
|
||||
n: ['#n_openai', 'n', false, false],
|
||||
bypass_status_check: ['#openai_bypass_status_check', 'bypass_status_check', true, true],
|
||||
request_images: ['#openai_request_images', 'request_images', true, false],
|
||||
azure_base_url: ['#azure_base_url', 'azure_base_url', false, true],
|
||||
azure_deployment_name: ['#azure_deployment_name', 'azure_deployment_name', false, true],
|
||||
azure_api_version: ['#azure_api_version', 'azure_api_version', false, true],
|
||||
azure_openai_model: ['#azure_openai_model', 'azure_openai_model', false, true],
|
||||
extensions: ['#NULL_SELECTOR', 'extensions', false, false],
|
||||
};
|
||||
|
||||
@@ -380,6 +387,10 @@ const default_settings = {
|
||||
cometapi_model: 'gpt-4o',
|
||||
moonshot_model: 'kimi-latest',
|
||||
fireworks_model: 'accounts/fireworks/models/kimi-k2-instruct',
|
||||
azure_base_url: '',
|
||||
azure_deployment_name: '',
|
||||
azure_api_version: '2024-02-15-preview',
|
||||
azure_openai_model: '',
|
||||
custom_model: '',
|
||||
custom_url: '',
|
||||
custom_include_body: '',
|
||||
@@ -470,6 +481,10 @@ const oai_settings = {
|
||||
cometapi_model: 'gpt-4o',
|
||||
moonshot_model: 'kimi-latest',
|
||||
fireworks_model: 'accounts/fireworks/models/kimi-k2-instruct',
|
||||
azure_base_url: '',
|
||||
azure_deployment_name: '',
|
||||
azure_api_version: '2024-02-15-preview',
|
||||
azure_openai_model: '',
|
||||
custom_model: '',
|
||||
custom_url: '',
|
||||
custom_include_body: '',
|
||||
@@ -1641,6 +1656,8 @@ export function getChatCompletionModel(source = null) {
|
||||
return oai_settings.moonshot_model;
|
||||
case chat_completion_sources.FIREWORKS:
|
||||
return oai_settings.fireworks_model;
|
||||
case chat_completion_sources.AZURE_OPENAI:
|
||||
return oai_settings.azure_openai_model;
|
||||
default:
|
||||
console.error(`Unknown chat completion source: ${activeSource}`);
|
||||
return '';
|
||||
@@ -1957,6 +1974,16 @@ function saveModelList(data) {
|
||||
|
||||
$('#model_cometapi_select').val(oai_settings.cometapi_model).trigger('change');
|
||||
}
|
||||
|
||||
if (oai_settings.chat_completion_source == chat_completion_sources.AZURE_OPENAI) {
|
||||
const modelId = model_list?.[0]?.id || '';
|
||||
oai_settings.azure_openai_model = modelId;
|
||||
|
||||
$('#azure_openai_model')
|
||||
.empty()
|
||||
.append(new Option(modelId || 'None', modelId || '', true, true))
|
||||
.trigger('change');
|
||||
}
|
||||
}
|
||||
|
||||
function appendOpenRouterOptions(model_list, groupModels = false, sort = false) {
|
||||
@@ -2068,6 +2095,7 @@ function getReasoningEffort() {
|
||||
// These sources expect the effort as string.
|
||||
const reasoningEffortSources = [
|
||||
chat_completion_sources.OPENAI,
|
||||
chat_completion_sources.AZURE_OPENAI,
|
||||
chat_completion_sources.CUSTOM,
|
||||
chat_completion_sources.XAI,
|
||||
chat_completion_sources.AIMLAPI,
|
||||
@@ -2087,7 +2115,7 @@ function getReasoningEffort() {
|
||||
case reasoning_effort_types.auto:
|
||||
return undefined;
|
||||
case reasoning_effort_types.min:
|
||||
return chat_completion_sources.OPENAI === oai_settings.chat_completion_source && /^gpt-5/.test(oai_settings.openai_model)
|
||||
return [chat_completion_sources.OPENAI, chat_completion_sources.AZURE_OPENAI].includes(oai_settings.chat_completion_source) && /^gpt-5/.test(getChatCompletionModel())
|
||||
? reasoning_effort_types.min
|
||||
: reasoning_effort_types.low;
|
||||
case reasoning_effort_types.max:
|
||||
@@ -2154,15 +2182,16 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } =
|
||||
const isXAI = oai_settings.chat_completion_source == chat_completion_sources.XAI;
|
||||
const isPollinations = oai_settings.chat_completion_source == chat_completion_sources.POLLINATIONS;
|
||||
const isMoonshot = oai_settings.chat_completion_source == chat_completion_sources.MOONSHOT;
|
||||
const isAzureOpenAI = oai_settings.chat_completion_source == chat_completion_sources.AZURE_OPENAI; // Add this line
|
||||
const isTextCompletion = isOAI && textCompletionModels.includes(oai_settings.openai_model);
|
||||
const isQuiet = type === 'quiet';
|
||||
const isImpersonate = type === 'impersonate';
|
||||
const isContinue = type === 'continue';
|
||||
const stream = oai_settings.stream_openai && !isQuiet && !(isOAI && ['o1-2024-12-17', 'o1'].includes(oai_settings.openai_model));
|
||||
const stream = oai_settings.stream_openai && !isQuiet && !((isOAI || isAzureOpenAI) && ['o1-2024-12-17', 'o1'].includes(getChatCompletionModel()));
|
||||
const useLogprobs = !!power_user.request_token_probabilities;
|
||||
const canMultiSwipe = oai_settings.n > 1 && !isContinue && !isImpersonate && !isQuiet && (isOAI || isCustom || isXAI || isAimlapi || isMoonshot);
|
||||
const canMultiSwipe = oai_settings.n > 1 && !isContinue && !isImpersonate && !isQuiet && (isOAI || isAzureOpenAI || isCustom || isXAI || isAimlapi || isMoonshot);
|
||||
|
||||
const logitBiasSources = [chat_completion_sources.OPENAI, chat_completion_sources.OPENROUTER, chat_completion_sources.CUSTOM];
|
||||
const logitBiasSources = [chat_completion_sources.OPENAI, chat_completion_sources.AZURE_OPENAI, chat_completion_sources.OPENROUTER, chat_completion_sources.CUSTOM];
|
||||
if (oai_settings.bias_preset_selected
|
||||
&& logitBiasSources.includes(oai_settings.chat_completion_source)
|
||||
&& Array.isArray(oai_settings.bias_presets[oai_settings.bias_preset_selected])
|
||||
@@ -2200,6 +2229,16 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } =
|
||||
'custom_prompt_post_processing': oai_settings.custom_prompt_post_processing,
|
||||
};
|
||||
|
||||
if (isAzureOpenAI) {
|
||||
generate_data.azure_base_url = oai_settings.azure_base_url;
|
||||
generate_data.azure_deployment_name = oai_settings.azure_deployment_name;
|
||||
generate_data.azure_api_version = oai_settings.azure_api_version;
|
||||
// Reasoning effort is not supported on some Azure models (e.g. GPT-3.x, GPT-4.x)
|
||||
if (/^gpt-[34]/.test(oai_settings.azure_openai_model)) {
|
||||
delete generate_data.reasoning_effort;
|
||||
}
|
||||
}
|
||||
|
||||
if (!canMultiSwipe && ToolManager.canPerformToolCalls(type)) {
|
||||
await ToolManager.registerFunctionToolsOpenAI(generate_data);
|
||||
}
|
||||
@@ -2217,18 +2256,18 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } =
|
||||
}
|
||||
|
||||
// Add logprobs request (currently OpenAI only, max 5 on their side)
|
||||
if (useLogprobs && (isOAI || isCustom || isDeepSeek || isXAI || isAimlapi)) {
|
||||
if (useLogprobs && (isOAI || isAzureOpenAI || isCustom || isDeepSeek || isXAI || isAimlapi)) {
|
||||
generate_data['logprobs'] = 5;
|
||||
}
|
||||
|
||||
// Remove logit bias/logprobs/stop-strings if not supported by the model
|
||||
const isVision = (m) => ['gpt', 'vision'].every(x => m.includes(x));
|
||||
if (isOAI && isVision(oai_settings.openai_model) || isOpenRouter && isVision(oai_settings.openrouter_model)) {
|
||||
if ((isOAI && isVision(oai_settings.openai_model)) || (isAzureOpenAI && isVision(oai_settings.azure_openai_model)) || (isOpenRouter && isVision(oai_settings.openrouter_model))) {
|
||||
delete generate_data.logit_bias;
|
||||
delete generate_data.stop;
|
||||
delete generate_data.logprobs;
|
||||
}
|
||||
if (isOAI && oai_settings.openai_model.includes('gpt-4.5') || isOpenRouter && oai_settings.openrouter_model.includes('gpt-4.5')) {
|
||||
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'))) {
|
||||
delete generate_data.logprobs;
|
||||
}
|
||||
|
||||
@@ -2341,6 +2380,7 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } =
|
||||
|
||||
const seedSupportedSources = [
|
||||
chat_completion_sources.OPENAI,
|
||||
chat_completion_sources.AZURE_OPENAI,
|
||||
chat_completion_sources.OPENROUTER,
|
||||
chat_completion_sources.MISTRALAI,
|
||||
chat_completion_sources.CUSTOM,
|
||||
@@ -2358,7 +2398,7 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } =
|
||||
generate_data['seed'] = oai_settings.seed;
|
||||
}
|
||||
|
||||
if (isOAI && /^(o1|o3|o4)/.test(oai_settings.openai_model)) {
|
||||
if ((isOAI && /^(o1|o3|o4)/.test(oai_settings.openai_model)) || (isAzureOpenAI && /^(o1|o3|o4)/.test(oai_settings.azure_openai_model))) {
|
||||
generate_data.max_completion_tokens = generate_data.max_tokens;
|
||||
delete generate_data.max_tokens;
|
||||
delete generate_data.logprobs;
|
||||
@@ -2381,7 +2421,7 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } =
|
||||
}
|
||||
}
|
||||
|
||||
if (isOAI && /^gpt-5/.test(oai_settings.openai_model)) {
|
||||
if ((isOAI && /^gpt-5/.test(oai_settings.openai_model)) || (isAzureOpenAI && /^gpt-5/.test(oai_settings.azure_openai_model))) {
|
||||
generate_data.max_completion_tokens = generate_data.max_tokens;
|
||||
delete generate_data.max_tokens;
|
||||
delete generate_data.logprobs;
|
||||
@@ -2551,6 +2591,7 @@ function parseChatCompletionLogprobs(data) {
|
||||
|
||||
switch (oai_settings.chat_completion_source) {
|
||||
case chat_completion_sources.OPENAI:
|
||||
case chat_completion_sources.AZURE_OPENAI:
|
||||
case chat_completion_sources.DEEPSEEK:
|
||||
case chat_completion_sources.XAI:
|
||||
case chat_completion_sources.CUSTOM:
|
||||
@@ -2559,7 +2600,7 @@ function parseChatCompletionLogprobs(data) {
|
||||
}
|
||||
// OpenAI Text Completion API is treated as a chat completion source
|
||||
// by SillyTavern, hence its presence in this function.
|
||||
return textCompletionModels.includes(oai_settings.openai_model)
|
||||
return textCompletionModels.includes(getChatCompletionModel())
|
||||
? parseOpenAITextLogprobs(data.choices[0]?.logprobs)
|
||||
: parseOpenAIChatLogprobs(data.choices[0]?.logprobs);
|
||||
default:
|
||||
@@ -3497,6 +3538,10 @@ function loadOpenAISettings(data, settings) {
|
||||
oai_settings.custom_include_headers = settings.custom_include_headers ?? default_settings.custom_include_headers;
|
||||
oai_settings.custom_prompt_post_processing = settings.custom_prompt_post_processing ?? default_settings.custom_prompt_post_processing;
|
||||
oai_settings.google_model = settings.google_model ?? default_settings.google_model;
|
||||
oai_settings.azure_base_url = settings.azure_base_url ?? default_settings.azure_base_url;
|
||||
oai_settings.azure_deployment_name = settings.azure_deployment_name ?? default_settings.azure_deployment_name;
|
||||
oai_settings.azure_api_version = settings.azure_api_version ?? default_settings.azure_api_version;
|
||||
oai_settings.azure_openai_model = settings.azure_openai_model ?? default_settings.azure_openai_model;
|
||||
oai_settings.vertexai_model = settings.vertexai_model ?? default_settings.vertexai_model;
|
||||
oai_settings.chat_completion_source = settings.chat_completion_source ?? default_settings.chat_completion_source;
|
||||
oai_settings.show_external_models = settings.show_external_models ?? default_settings.show_external_models;
|
||||
@@ -3593,6 +3638,11 @@ function loadOpenAISettings(data, settings) {
|
||||
$(`#model_moonshot_select option[value="${oai_settings.moonshot_model}"`).prop('selected', true);
|
||||
$('#custom_model_id').val(oai_settings.custom_model);
|
||||
$('#custom_api_url_text').val(oai_settings.custom_url);
|
||||
$('#azure_base_url').val(oai_settings.azure_base_url);
|
||||
$('#azure_deployment_name').val(oai_settings.azure_deployment_name);
|
||||
$('#azure_api_version').val(oai_settings.azure_api_version);
|
||||
$('#azure_openai_model').val(oai_settings.azure_openai_model);
|
||||
|
||||
$('#openai_max_context').val(oai_settings.openai_max_context);
|
||||
$('#openai_max_context_counter').val(`${oai_settings.openai_max_context}`);
|
||||
$('#model_openrouter_select').val(oai_settings.openrouter_model);
|
||||
@@ -3771,6 +3821,12 @@ async function getStatusOpen() {
|
||||
return resultCheckStatus();
|
||||
}
|
||||
|
||||
if (oai_settings.chat_completion_source === chat_completion_sources.AZURE_OPENAI && !isValidUrl(oai_settings.azure_base_url)) {
|
||||
console.debug('Invalid endpoint URL of Azure OpenAI API:', oai_settings.azure_base_url);
|
||||
setOnlineStatus(t`Invalid Azure endpoint URL. Requests may fail.`);
|
||||
return resultCheckStatus();
|
||||
}
|
||||
|
||||
let data = {
|
||||
reverse_proxy: oai_settings.reverse_proxy,
|
||||
proxy_password: oai_settings.proxy_password,
|
||||
@@ -3796,6 +3852,12 @@ async function getStatusOpen() {
|
||||
data.custom_include_headers = oai_settings.custom_include_headers;
|
||||
}
|
||||
|
||||
if (oai_settings.chat_completion_source === chat_completion_sources.AZURE_OPENAI) {
|
||||
data.azure_base_url = oai_settings.azure_base_url;
|
||||
data.azure_deployment_name = oai_settings.azure_deployment_name;
|
||||
data.azure_api_version = oai_settings.azure_api_version;
|
||||
}
|
||||
|
||||
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;
|
||||
if (canBypass) {
|
||||
setOnlineStatus(t`Status check bypassed`);
|
||||
@@ -3877,6 +3939,10 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
|
||||
custom_prompt_post_processing: settings.custom_prompt_post_processing,
|
||||
google_model: settings.google_model,
|
||||
vertexai_model: settings.vertexai_model,
|
||||
azure_base_url: settings.azure_base_url,
|
||||
azure_deployment_name: settings.azure_deployment_name,
|
||||
azure_api_version: settings.azure_api_version,
|
||||
azure_openai_model: settings.azure_openai_model,
|
||||
temperature: settings.temp_openai,
|
||||
frequency_penalty: settings.freq_pen_openai,
|
||||
presence_penalty: settings.pres_pen_openai,
|
||||
@@ -4839,6 +4905,14 @@ async function onModelChange() {
|
||||
oai_settings.cometapi_model = value;
|
||||
}
|
||||
|
||||
if ($(this).is('#azure_openai_model')) {
|
||||
if (!value) {
|
||||
console.debug('Null Azure OpenAI model selected. Ignoring.');
|
||||
return;
|
||||
}
|
||||
oai_settings.azure_openai_model = value;
|
||||
}
|
||||
|
||||
if ([chat_completion_sources.MAKERSUITE, chat_completion_sources.VERTEXAI].includes(oai_settings.chat_completion_source)) {
|
||||
if (oai_settings.max_context_unlocked) {
|
||||
$('#openai_max_context').attr('max', max_2mil);
|
||||
@@ -4913,7 +4987,7 @@ async function onModelChange() {
|
||||
$('#temp_openai').attr('max', claude_max_temp).val(oai_settings.temp_openai).trigger('input');
|
||||
}
|
||||
|
||||
if (oai_settings.chat_completion_source == chat_completion_sources.OPENAI) {
|
||||
if ([chat_completion_sources.AZURE_OPENAI, chat_completion_sources.OPENAI].includes(oai_settings.chat_completion_source)) {
|
||||
$('#openai_max_context').attr('max', getMaxContextOpenAI(value));
|
||||
oai_settings.openai_max_context = Math.min(oai_settings.openai_max_context, Number($('#openai_max_context').attr('max')));
|
||||
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
|
||||
@@ -5419,6 +5493,20 @@ async function onConnectButtonClick(e) {
|
||||
}
|
||||
}
|
||||
|
||||
if (oai_settings.chat_completion_source == chat_completion_sources.AZURE_OPENAI) {
|
||||
const api_key_azure_openai = String($('#api_key_azure_openai').val()).trim();
|
||||
|
||||
if (api_key_azure_openai.length) {
|
||||
await writeSecret(SECRET_KEYS.AZURE_OPENAI, api_key_azure_openai);
|
||||
}
|
||||
|
||||
if (!api_key_azure_openai && !secret_state[SECRET_KEYS.AZURE_OPENAI]) {
|
||||
console.log('No secret key saved for Azure OpenAI');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
startStatusLoading();
|
||||
saveSettingsDebounced();
|
||||
await getStatusOpen();
|
||||
@@ -5492,6 +5580,9 @@ function toggleChatCompletionForms() {
|
||||
else if (oai_settings.chat_completion_source == chat_completion_sources.COMETAPI) {
|
||||
$('#model_cometapi_select').trigger('change');
|
||||
}
|
||||
else if (oai_settings.chat_completion_source == chat_completion_sources.AZURE_OPENAI) {
|
||||
$('#azure_openai_model').trigger('change');
|
||||
}
|
||||
|
||||
$('[data-source]').each(function () {
|
||||
const validSources = $(this).data('source').split(',');
|
||||
@@ -5611,10 +5702,15 @@ export function isImageInliningSupported() {
|
||||
|
||||
switch (oai_settings.chat_completion_source) {
|
||||
case chat_completion_sources.OPENAI:
|
||||
case chat_completion_sources.AZURE_OPENAI: {
|
||||
const modelToCheck = oai_settings.chat_completion_source === chat_completion_sources.AZURE_OPENAI
|
||||
? oai_settings.azure_openai_model
|
||||
: oai_settings.openai_model;
|
||||
return visionSupportedModels.some(model =>
|
||||
oai_settings.openai_model.includes(model)
|
||||
&& ['gpt-4-turbo-preview', 'o1-mini', 'o3-mini'].some(x => !oai_settings.openai_model.includes(x)),
|
||||
modelToCheck.includes(model)
|
||||
&& ['gpt-4-turbo-preview', 'o1-mini', 'o3-mini'].some(x => !modelToCheck.includes(x)),
|
||||
);
|
||||
}
|
||||
case chat_completion_sources.MAKERSUITE:
|
||||
return visionSupportedModels.some(model => oai_settings.google_model.includes(model));
|
||||
case chat_completion_sources.VERTEXAI:
|
||||
@@ -6295,6 +6391,21 @@ export function initOpenAI() {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#azure_base_url').on('input', function () {
|
||||
oai_settings.azure_base_url = String($(this).val());
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#azure_deployment_name').on('input', function () {
|
||||
oai_settings.azure_deployment_name = String($(this).val());
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#azure_api_version').on('input change', function () {
|
||||
oai_settings.azure_api_version = String($(this).val());
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#character_names_none').on('input', function () {
|
||||
oai_settings.names_behavior = character_names_behavior.NONE;
|
||||
setNamesBehaviorControls();
|
||||
@@ -6453,6 +6564,7 @@ export function initOpenAI() {
|
||||
$('#model_cometapi_select').on('change', onModelChange);
|
||||
$('#model_moonshot_select').on('change', onModelChange);
|
||||
$('#model_fireworks_select').on('change', onModelChange);
|
||||
$('#azure_openai_model').on('change', onModelChange);
|
||||
$('#settings_preset_openai').on('change', onSettingsPresetChange);
|
||||
$('#new_oai_preset').on('click', onNewPresetClick);
|
||||
$('#delete_oai_preset').on('click', onDeletePresetClick);
|
||||
|
||||
@@ -47,6 +47,7 @@ export const SECRET_KEYS = {
|
||||
PERPLEXITY: 'api_key_perplexity',
|
||||
GROQ: 'api_key_groq',
|
||||
AZURE_TTS: 'api_key_azure_tts',
|
||||
AZURE_OPENAI: 'api_key_azure_openai',
|
||||
FEATHERLESS: 'api_key_featherless',
|
||||
HUGGINGFACE: 'api_key_huggingface',
|
||||
STABILITY: 'api_key_stability',
|
||||
@@ -122,6 +123,7 @@ const FRIENDLY_NAMES = {
|
||||
[SECRET_KEYS.MINIMAX_GROUP_ID]: 'MiniMax Group ID',
|
||||
[SECRET_KEYS.MOONSHOT]: 'Moonshot AI',
|
||||
[SECRET_KEYS.COMETAPI]: 'CometAPI',
|
||||
[SECRET_KEYS.AZURE_OPENAI]: 'Azure OpenAI',
|
||||
};
|
||||
|
||||
const INPUT_MAP = {
|
||||
@@ -160,6 +162,7 @@ const INPUT_MAP = {
|
||||
[SECRET_KEYS.MOONSHOT]: '#api_key_moonshot',
|
||||
[SECRET_KEYS.FIREWORKS]: '#api_key_fireworks',
|
||||
[SECRET_KEYS.COMETAPI]: '#api_key_cometapi',
|
||||
[SECRET_KEYS.AZURE_OPENAI]: '#api_key_azure_openai',
|
||||
};
|
||||
|
||||
const getLabel = () => moment().format('L LT');
|
||||
|
||||
@@ -586,6 +586,10 @@ export function getTokenizerModel() {
|
||||
const nemoTokenizer = 'nemo';
|
||||
const deepseekTokenizer = 'deepseek';
|
||||
|
||||
if (oai_settings.chat_completion_source == chat_completion_sources.AZURE_OPENAI) {
|
||||
return oai_settings.azure_openai_model || turboTokenizer;
|
||||
}
|
||||
|
||||
if (oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK) {
|
||||
return deepseekTokenizer;
|
||||
}
|
||||
|
||||
@@ -659,6 +659,7 @@ export class ToolManager {
|
||||
chat_completion_sources.FIREWORKS,
|
||||
chat_completion_sources.COMETAPI,
|
||||
chat_completion_sources.ELECTRONHUB,
|
||||
chat_completion_sources.AZURE_OPENAI,
|
||||
];
|
||||
return supportedSources.includes(oai_settings.chat_completion_source);
|
||||
}
|
||||
|
||||
@@ -182,6 +182,7 @@ export const CHAT_COMPLETION_SOURCES = {
|
||||
MOONSHOT: 'moonshot',
|
||||
FIREWORKS: 'fireworks',
|
||||
COMETAPI: 'cometapi',
|
||||
AZURE_OPENAI: 'azure_openai',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -408,6 +409,45 @@ export const VLLM_KEYS = [
|
||||
'guided_whitespace_pattern',
|
||||
];
|
||||
|
||||
export const AZURE_OPENAI_KEYS = [
|
||||
'messages',
|
||||
'temperature',
|
||||
'frequency_penalty',
|
||||
'presence_penalty',
|
||||
'top_p',
|
||||
'max_tokens',
|
||||
'max_completion_tokens',
|
||||
'stream',
|
||||
'logit_bias',
|
||||
'stop',
|
||||
'n',
|
||||
'logprobs',
|
||||
'seed',
|
||||
'tools',
|
||||
'tool_choice',
|
||||
'reasoning_effort',
|
||||
];
|
||||
|
||||
export const OPENAI_REASONING_EFFORT_MODELS = [
|
||||
'o1',
|
||||
'o3-mini',
|
||||
'o3-mini-2025-01-31',
|
||||
'o4-mini',
|
||||
'o4-mini-2025-04-16',
|
||||
'o3',
|
||||
'o3-2025-04-16',
|
||||
'gpt-5',
|
||||
'gpt-5-2025-08-07',
|
||||
'gpt-5-mini',
|
||||
'gpt-5-mini-2025-08-07',
|
||||
'gpt-5-nano',
|
||||
'gpt-5-nano-2025-08-07',
|
||||
];
|
||||
|
||||
export const OPENAI_REASONING_EFFORT_MAP = {
|
||||
min: 'minimal',
|
||||
};
|
||||
|
||||
export const LOG_LEVELS = {
|
||||
DEBUG: 0,
|
||||
INFO: 1,
|
||||
|
||||
@@ -6,8 +6,11 @@ import urlJoin from 'url-join';
|
||||
|
||||
import {
|
||||
AIMLAPI_HEADERS,
|
||||
AZURE_OPENAI_KEYS,
|
||||
CHAT_COMPLETION_SOURCES,
|
||||
GEMINI_SAFETY,
|
||||
OPENAI_REASONING_EFFORT_MAP,
|
||||
OPENAI_REASONING_EFFORT_MODELS,
|
||||
OPENROUTER_HEADERS,
|
||||
} from '../../constants.js';
|
||||
import {
|
||||
@@ -1294,6 +1297,100 @@ async function sendElectronHubRequest(request, response) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a chat completion request to Azure OpenAI.
|
||||
* @param {express.Request} request Express request object (contains request.body with all generate_data)
|
||||
* @param {express.Response} response Express response object
|
||||
*/
|
||||
async function sendAzureOpenAIRequest(request, response) {
|
||||
// 1. GATHER & VALIDATE SETTINGS
|
||||
const { azure_base_url, azure_deployment_name, azure_api_version } = request.body;
|
||||
const apiKey = readSecret(request.user.directories, SECRET_KEYS.AZURE_OPENAI);
|
||||
if (!azure_base_url || !azure_deployment_name || !azure_api_version || !apiKey) {
|
||||
return response.status(400).send({
|
||||
error: {
|
||||
message: 'Azure OpenAI configuration is incomplete. Please provide Base URL, Deployment Name, API Version, and API Key in the connection settings.',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 2. PREPARE THE REQUEST
|
||||
const url = new URL(`/openai/deployments/${azure_deployment_name}/chat/completions`, azure_base_url);
|
||||
url.searchParams.set('api-version', azure_api_version);
|
||||
const endpointUrl = url.toString();
|
||||
|
||||
// Create the base payload with all standard parameters
|
||||
const apiRequestBody = /** @type {any} */ ({});
|
||||
for (const key of AZURE_OPENAI_KEYS) {
|
||||
if (Object.hasOwn(request.body, key)) {
|
||||
apiRequestBody[key] = request.body[key];
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Structured Output (JSON Mode) by translating the custom `json_schema` object.
|
||||
if (request.body.json_schema) {
|
||||
apiRequestBody['response_format'] = {
|
||||
type: 'json_schema',
|
||||
json_schema: {
|
||||
name: request.body.json_schema.name,
|
||||
strict: request.body.json_schema.strict ?? true,
|
||||
schema: request.body.json_schema.value,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Adjust logprobs for Azure OpenAI, which follows the OpenAI Chat Completions API spec.
|
||||
if (typeof apiRequestBody.logprobs === 'number' && apiRequestBody.logprobs > 0) {
|
||||
apiRequestBody.top_logprobs = apiRequestBody.logprobs;
|
||||
apiRequestBody.logprobs = true;
|
||||
}
|
||||
|
||||
// Do not send reasoning effort to models which do not support it
|
||||
apiRequestBody['reasoning_effort'] = OPENAI_REASONING_EFFORT_MODELS.includes(request.body.model)
|
||||
? OPENAI_REASONING_EFFORT_MAP[request.body.reasoning_effort] ?? request.body.reasoning_effort
|
||||
: undefined;
|
||||
|
||||
const controller = new AbortController();
|
||||
request.socket.removeAllListeners('close');
|
||||
request.socket.on('close', () => controller.abort());
|
||||
|
||||
const config = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'api-key': apiKey,
|
||||
},
|
||||
body: JSON.stringify(apiRequestBody),
|
||||
signal: controller.signal,
|
||||
};
|
||||
|
||||
console.info(`Sending request to Azure OpenAI: ${endpointUrl}`);
|
||||
console.debug('Azure OpenAI Request Body:', apiRequestBody);
|
||||
try {
|
||||
const fetchResponse = await fetch(endpointUrl, config);
|
||||
|
||||
if (request.body.stream) {
|
||||
return forwardFetchResponse(fetchResponse, response);
|
||||
}
|
||||
|
||||
if (fetchResponse.ok) {
|
||||
/** @type {any} */
|
||||
const json = await fetchResponse.json();
|
||||
console.debug('Azure OpenAI response:', json);
|
||||
return response.send(json);
|
||||
}
|
||||
|
||||
const text = await fetchResponse.text();
|
||||
const data = tryParse(text) || { error: { message: fetchResponse.statusText || 'Unknown error occurred' } };
|
||||
return response.status(500).send(data);
|
||||
} catch (error) {
|
||||
const message = error.name === 'AbortError'
|
||||
? 'Request was aborted by the client.'
|
||||
: (error.message || 'An unknown network error occurred.');
|
||||
return response.status(500).send({ error: { message, ...error } });
|
||||
}
|
||||
}
|
||||
|
||||
export const router = express.Router();
|
||||
|
||||
router.post('/status', async function (request, statusResponse) {
|
||||
@@ -1404,6 +1501,84 @@ router.post('/status', async function (request, statusResponse) {
|
||||
console.error('Error fetching Google AI Studio models:', error);
|
||||
return statusResponse.send({ error: true, bypass: true, data: { data: [] } });
|
||||
}
|
||||
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.AZURE_OPENAI) {
|
||||
const { azure_base_url, azure_deployment_name, azure_api_version } = request.body;
|
||||
const apiKey = readSecret(request.user.directories, SECRET_KEYS.AZURE_OPENAI);
|
||||
|
||||
// 1) Validate configuration from the frontend
|
||||
if (!apiKey || !azure_base_url || !azure_deployment_name || !azure_api_version) {
|
||||
console.warn('Azure OpenAI status check failed: missing config from frontend.');
|
||||
return statusResponse.status(400).send({ error: true, message: 'Azure configuration is incomplete.' });
|
||||
}
|
||||
// 2) Build URLs using the URL API for consistency and robustness.
|
||||
const modelsUrl = new URL('/openai/models', azure_base_url);
|
||||
modelsUrl.searchParams.set('api-version', azure_api_version);
|
||||
|
||||
const chatUrl = new URL(`/openai/deployments/${azure_deployment_name}/chat/completions`, azure_base_url);
|
||||
chatUrl.searchParams.set('api-version', azure_api_version);
|
||||
|
||||
// Map common status codes to user-friendly error messages
|
||||
const azureStatusErrorMap = {
|
||||
400: 'API version may be invalid for this resource.',
|
||||
401: 'Invalid API key or insufficient permissions.',
|
||||
403: 'Invalid API key or insufficient permissions.',
|
||||
404: 'Endpoint URL appears incorrect (404).',
|
||||
};
|
||||
|
||||
try {
|
||||
// ---- A) GET /models: fast sanity check for endpoint + api key + api version ----
|
||||
const apiConfigTest = await fetch(modelsUrl, {
|
||||
method: 'GET',
|
||||
headers: { 'api-key': apiKey, 'Accept': 'application/json' },
|
||||
});
|
||||
|
||||
if (!apiConfigTest.ok) {
|
||||
let errText = '';
|
||||
try { errText = await apiConfigTest.text(); } catch { /* response body may be empty */ }
|
||||
|
||||
console.warn('Azure OpenAI GET /models failed:', apiConfigTest.status, apiConfigTest.statusText, errText || '');
|
||||
|
||||
const defaultMessage = `Azure Models endpoint error: ${apiConfigTest.statusText}`;
|
||||
const message = azureStatusErrorMap[apiConfigTest.status] ?? defaultMessage;
|
||||
return statusResponse.status(apiConfigTest.status).send({ error: true, message });
|
||||
}
|
||||
|
||||
// ---- B) POST /chat/completions: verify deployment + read underlying model ID ----
|
||||
// Small, deterministic probe to minimize cost/latency
|
||||
const modelPayload = {
|
||||
messages: [{ role: 'user', content: 'Say word Hi' }],
|
||||
stream: false,
|
||||
max_completion_tokens: 5,
|
||||
};
|
||||
|
||||
const modelRequest = await fetch(chatUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'api-key': apiKey, 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
||||
body: JSON.stringify(modelPayload),
|
||||
});
|
||||
|
||||
let modelResponse;
|
||||
try {
|
||||
modelResponse = await modelRequest.json();
|
||||
} catch {
|
||||
modelResponse = { raw: 'Failed to parse JSON response from chat completions probe.' };
|
||||
}
|
||||
|
||||
const modelId = /** @type {any} */ (modelResponse)?.model;
|
||||
if (!modelId) {
|
||||
console.warn('Azure status check succeeded but could not find a model ID in the response.');
|
||||
console.debug('Azure Response Body:', modelResponse);
|
||||
// Keep a benign success to avoid UX disruption in the UI
|
||||
return statusResponse.send({ data: [] });
|
||||
}
|
||||
|
||||
console.info(color.green('Azure OpenAI connection successful. Detected model:'), modelId);
|
||||
// Consistent response format: always an array of { id }
|
||||
return statusResponse.send({ data: [{ id: modelId }] });
|
||||
} catch (error) {
|
||||
console.error('Azure OpenAI status check connection error:', error);
|
||||
return statusResponse.status(500).send({ error: true, message: 'Failed to connect to the Azure endpoint.' });
|
||||
}
|
||||
} else {
|
||||
console.warn('This chat completion source is not supported yet.');
|
||||
return statusResponse.status(400).send({ error: true });
|
||||
@@ -1596,6 +1771,7 @@ router.post('/generate', function (request, response) {
|
||||
case CHAT_COMPLETION_SOURCES.AIMLAPI: return sendAimlapiRequest(request, response);
|
||||
case CHAT_COMPLETION_SOURCES.XAI: return sendXaiRequest(request, response);
|
||||
case CHAT_COMPLETION_SOURCES.ELECTRONHUB: return sendElectronHubRequest(request, response);
|
||||
case CHAT_COMPLETION_SOURCES.AZURE_OPENAI: return sendAzureOpenAIRequest(request, response);
|
||||
}
|
||||
|
||||
let apiUrl;
|
||||
@@ -1803,26 +1979,8 @@ router.post('/generate', function (request, response) {
|
||||
|
||||
// A few of OpenAIs reasoning models support reasoning effort
|
||||
if (request.body.reasoning_effort && [CHAT_COMPLETION_SOURCES.CUSTOM, CHAT_COMPLETION_SOURCES.OPENAI].includes(request.body.chat_completion_source)) {
|
||||
const reasoningEffortModels = [
|
||||
'o1',
|
||||
'o3-mini',
|
||||
'o3-mini-2025-01-31',
|
||||
'o4-mini',
|
||||
'o4-mini-2025-04-16',
|
||||
'o3',
|
||||
'o3-2025-04-16',
|
||||
'gpt-5',
|
||||
'gpt-5-2025-08-07',
|
||||
'gpt-5-mini',
|
||||
'gpt-5-mini-2025-08-07',
|
||||
'gpt-5-nano',
|
||||
'gpt-5-nano-2025-08-07',
|
||||
];
|
||||
const reasoningEffortMap = {
|
||||
min: 'minimal',
|
||||
};
|
||||
if (reasoningEffortModels.includes(request.body.model)) {
|
||||
bodyParams['reasoning_effort'] = reasoningEffortMap[request.body.reasoning_effort] ?? request.body.reasoning_effort;
|
||||
if (OPENAI_REASONING_EFFORT_MODELS.includes(request.body.model)) {
|
||||
bodyParams['reasoning_effort'] = OPENAI_REASONING_EFFORT_MAP[request.body.reasoning_effort] ?? request.body.reasoning_effort;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ export const SECRET_KEYS = {
|
||||
MINIMAX_GROUP_ID: 'minimax_group_id',
|
||||
MOONSHOT: 'api_key_moonshot',
|
||||
COMETAPI: 'api_key_cometapi',
|
||||
AZURE_OPENAI: 'api_key_azure_openai',
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user