Merge pull request #3839 from SillyTavern/gork-ai xAI grok

4dfc33b2a071c29461e29784ebdb4ca8787ee55a

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
19 files changed, +342 -14Showing whitespace changes
public/img/xai.svg+46 -0
@@ -0,0 +1,46 @@
1+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2+<!-- Generator: Adobe Illustrator 27.5.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
3+
4+<svg
5+ version="1.1"
6+ id="katman_1"
7+ x="0px"
8+ y="0px"
9+ viewBox="0 0 438.67001 481.44999"
10+ xml:space="preserve"
11+ sodipodi:docname="XAI_Logo.svg"
12+ width="438.67001"
13+ height="481.45001"
14+ inkscape:version="1.3 (0e150ed, 2023-07-21)"
15+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
16+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
17+ xmlns="http://www.w3.org/2000/svg"
18+ xmlns:svg="http://www.w3.org/2000/svg"><defs
19+ id="defs4" /><sodipodi:namedview
20+ id="namedview4"
21+ pagecolor="#ffffff"
22+ bordercolor="#000000"
23+ borderopacity="0.25"
24+ inkscape:showpageshadow="2"
25+ inkscape:pageopacity="0.0"
26+ inkscape:pagecheckerboard="0"
27+ inkscape:deskcolor="#d1d1d1"
28+ inkscape:zoom="0.39645207"
29+ inkscape:cx="219.44645"
30+ inkscape:cy="238.36425"
31+ inkscape:window-width="1512"
32+ inkscape:window-height="856"
33+ inkscape:window-x="0"
34+ inkscape:window-y="38"
35+ inkscape:window-maximized="1"
36+ inkscape:current-layer="katman_1" />&#10;<g
37+ id="g4"
38+ transform="translate(-201.61,-56.91)">&#10; <polygon
39+ points="631.96,538.36 640.28,93.18 557.09,211.99 565.4,538.36 "
40+ id="polygon1" />&#10; <polygon
41+ points="379.35,284.53 430.13,357.05 640.28,56.91 538.72,56.91 "
42+ id="polygon2" />&#10; <polygon
43+ points="353.96,465.84 303.17,393.31 201.61,538.36 303.17,538.36 "
44+ id="polygon3" />&#10; <polygon
45+ points="531.69,538.36 303.17,211.99 201.61,211.99 430.13,538.36 "
46+ id="polygon4" />&#10;</g>&#10;</svg>
public/index.html+36 -10
@@ -685,7 +685,7 @@
685685 </span>
686686 </div>
687687 </div>
688688 <div class="range-block" data-source="openai,claude,windowai,openrouter,ai21,scale,makersuite,mistralai,custom,cohere,perplexity,groq,01ai,nanogpt,deepseek,xai">
689689 <div class="range-block-title" data-i18n="Temperature">
690690 Temperature
691691 </div>
@@ -698,7 +698,7 @@
698698 </div>
699699 </div>
700700 </div>
701701 <div class="range-block" data-source="openai,openrouter,custom,cohere,perplexity,groq,mistralai,nanogpt,deepseek,xai">
702702 <div class="range-block-title" data-i18n="Frequency Penalty">
703703 Frequency Penalty
704704 </div>
@@ -711,7 +711,7 @@
711711 </div>
712712 </div>
713713 </div>
714714 <div class="range-block" data-source="openai,openrouter,custom,cohere,perplexity,groq,mistralai,nanogpt,deepseek,xai">
715715 <div class="range-block-title" data-i18n="Presence Penalty">
716716 Presence Penalty
717717 </div>
@@ -737,7 +737,7 @@
737737 </div>
738738 </div>
739739 </div>
740740 <div class="range-block" data-source="openai,claude,openrouter,ai21,scale,makersuite,mistralai,custom,cohere,perplexity,groq,01ai,nanogpt,deepseek,xai">
741741 <div class="range-block-title" data-i18n="Top P">
742742 Top P
743743 </div>
@@ -974,7 +974,7 @@
974974 </div>
975975 </div>
976976 </div>
977977 <div class="range-block" data-source="openai,openrouter,mistralai,custom,cohere,groq,nanogpt,xai">
978978 <div class="range-block-title justifyLeft" data-i18n="Seed">
979979 Seed
980980 </div>
@@ -1965,7 +1965,7 @@
19651965 </span>
19661966 </div>
19671967 </div>
19681968 <div class="range-block" data-source="openai,cohere,mistralai,custom,claude,openrouter,groq,deepseek,makersuite,ai21,xai">
19691969 <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">
19701970 <input id="openai_function_calling" type="checkbox" />
19711971 <span data-i18n="Enable function calling">Enable function calling</span>
@@ -1975,7 +1975,7 @@
19751975 <span data-i18n="enable_functions_desc_3">Can be utilized by various extensions to provide additional functionality.</span>
19761976 </div>
19771977 </div>
19781978 <div class="range-block" data-source="openai,openrouter,mistralai,makersuite,claude,custom,01ai,xai">
19791979 <label for="openai_image_inlining" class="checkbox_label flexWrap widthFreeExpand">
19801980 <input id="openai_image_inlining" type="checkbox" />
19811981 <span data-i18n="Send inline images">Send inline images</span>
@@ -1987,7 +1987,7 @@
19871987 <code><i class="fa-solid fa-wand-magic-sparkles"></i></code>
19881988 <span data-i18n="image_inlining_hint_3">menu to attach an image file to the chat.</span>
19891989 </div>
19901990 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,xai">
19911991 <div class="flex-container oneline-dropdown">
19921992 <label for="openai_inline_image_quality" data-i18n="Inline Image Quality">
19931993 Inline Image Quality
@@ -2031,7 +2031,7 @@
20312031 </span>
20322032 </div>
20332033 </div>
20342034 <div class="range-block" data-source="deepseek,openrouter,custom,claude,xai">
20352035 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">
20362036 <input id="openai_show_thoughts" type="checkbox" />
20372037 <span>
@@ -2045,7 +2045,7 @@
20452045 </span>
20462046 </div>
20472047 </div>
20482048 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,claude,xai">
20492049 <div class="flex-container oneline-dropdown" title="Constrains effort on reasoning for reasoning models.&#10;Currently supported values are low, medium, and high.&#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.">
20502050 <label for="openai_reasoning_effort">
20512051 <span data-i18n="Reasoning Effort">Reasoning Effort</span>
@@ -2756,6 +2756,7 @@
27562756 <option value="perplexity">Perplexity</option>
27572757 <option value="scale">Scale</option>
27582758 <option value="windowai">Window AI</option>
2759+ <option value="xai">xAI (Grok)</option>
27592760 </optgroup>
27602761 </select>
27612762 <div class="inline-drawer wide100p" data-source="openai,claude,mistralai,makersuite,deepseek">
@@ -3424,6 +3425,31 @@
34243425 <select id="model_01ai_select">
34253426 </select>
34263427 </div>
3428+ <div id="xai_form" data-source="xai">
3429+ <h4>
3430+ <a data-i18n="xAI API Key" href="https://console.x.ai/" target="_blank" rel="noopener noreferrer">
3431+ xAI API Key
3432+ </a>
3433+ </h4>
3434+ <div class="flex-container">
3435+ <input id="api_key_xai" name="api_key_xai" class="text_pole flex1" value="" type="text" autocomplete="off">
3436+ <div title="Clear your API key" data-i18n="[title]Clear your API key" class="menu_button fa-solid fa-circle-xmark clear-api-key" data-key="api_key_xai"></div>
3437+ </div>
3438+ <div data-for="api_key_xai" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you reload the page.">
3439+ For privacy reasons, your API key will be hidden after you reload the page.
3440+ </div>
3441+ <h4 data-i18n="xAI Model">xAI Model</h4>
3442+ <select id="model_xai_select">
3443+ <option value="grok-3-beta">grok-3-beta</option>
3444+ <option value="grok-3-fast-beta">grok-3-fast-beta</option>
3445+ <option value="grok-3-mini-beta">grok-3-mini-beta</option>
3446+ <option value="grok-3-mini-fast-beta">grok-3-mini-fast-beta</option>
3447+ <option value="grok-2-vision-1212">grok-2-vision-1212</option>
3448+ <option value="grok-2-1212">grok-2-1212</option>
3449+ <option value="grok-vision-beta">grok-vision-beta</option>
3450+ <option value="grok-beta">grok-beta</option>
3451+ </select>
3452+ </div>
34273453 <div id="prompt_post_porcessing_form" data-source="custom,openrouter">
34283454 <h4 data-i18n="Prompt Post-Processing">Prompt Post-Processing</h4>
34293455 <select id="custom_prompt_post_processing" class="text_pole" title="Applies additional processing to the prompt before sending it to the API." data-i18n="[title]Applies additional processing to the prompt before sending it to the API.">
public/scripts/RossAscends-mods.js+1 -0
@@ -409,6 +409,7 @@ function RA_autoconnect(PrevApi) {
409409 || (secret_state[SECRET_KEYS.ZEROONEAI] && oai_settings.chat_completion_source == chat_completion_sources.ZEROONEAI)
410410 || (secret_state[SECRET_KEYS.NANOGPT] && oai_settings.chat_completion_source == chat_completion_sources.NANOGPT)
411411 || (secret_state[SECRET_KEYS.DEEPSEEK] && oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK)
412+ || (secret_state[SECRET_KEYS.XAI] && oai_settings.chat_completion_source == chat_completion_sources.XAI)
412413 || (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM)
413414 ) {
414415 $('#api_button_openai').trigger('click');
public/scripts/extensions/caption/index.js+1 -0
@@ -428,6 +428,7 @@ jQuery(async function () {
428428 'zerooneai': SECRET_KEYS.ZEROONEAI,
429429 'groq': SECRET_KEYS.GROQ,
430430 'cohere': SECRET_KEYS.COHERE,
431+ 'xai': SECRET_KEYS.XAI,
431432 };
432433
433434 if (chatCompletionApis[api] && secret_state[chatCompletionApis[api]]) {
public/scripts/extensions/caption/settings.html+3 -0
@@ -31,6 +31,7 @@
3131 <option value="openrouter">OpenRouter</option>
3232 <option value="ooba" data-i18n="Text Generation WebUI (oobabooga)">Text Generation WebUI (oobabooga)</option>
3333 <option value="vllm">vLLM</option>
34+ <option value="xai">xAI (Grok)</option>
3435 </select>
3536 </div>
3637 <div class="flex1 flex-container flexFlowColumn flexNoGap">
@@ -134,6 +135,8 @@
134135 <option data-type="koboldcpp" value="koboldcpp_current" data-i18n="currently_loaded">[Currently loaded]</option>
135136 <option data-type="vllm" value="vllm_current" data-i18n="currently_selected">[Currently selected]</option>
136137 <option data-type="custom" value="custom_current" data-i18n="currently_selected">[Currently selected]</option>
138+ <option data-type="xai" value="grok-2-vision-1212">grok-2-vision-1212</option>
139+ <option data-type="xai" value="grok-vision-beta">grok-vision-beta</option>
137140 </select>
138141 </div>
139142 <div data-type="ollama">
public/scripts/extensions/shared.js+4 -0
@@ -153,6 +153,10 @@ function throwIfInvalidModel(useReverseProxy) {
153153 throw new Error('Cohere API key is not set.');
154154 }
155155
156+ if (extension_settings.caption.multimodal_api === 'xai' && !secret_state[SECRET_KEYS.XAI]) {
157+ throw new Error('xAI API key is not set.');
158+ }
159+
156160 if (extension_settings.caption.multimodal_api === 'ollama' && !textgenerationwebui_settings.server_urls[textgen_types.OLLAMA]) {
157161 throw new Error('Ollama server URL is not set.');
158162 }
public/scripts/extensions/stable-diffusion/index.js+55 -0
@@ -81,6 +81,7 @@ const sources = {
8181 nanogpt: 'nanogpt',
8282 bfl: 'bfl',
8383 falai: 'falai',
84+ xai: 'xai',
8485};
8586
8687const initiators = {
@@ -1303,6 +1304,7 @@ async function onModelChange() {
13031304 sources.nanogpt,
13041305 sources.bfl,
13051306 sources.falai,
1307+ sources.xai,
13061308 ];
13071309
13081310 if (cloudSources.includes(extension_settings.sd.source)) {
@@ -1518,6 +1520,9 @@ async function loadSamplers() {
15181520 case sources.bfl:
15191521 samplers = ['N/A'];
15201522 break;
1523+ case sources.xai:
1524+ samplers = ['N/A'];
1525+ break;
15211526 }
15221527
15231528 for (const sampler of samplers) {
@@ -1708,6 +1713,9 @@ async function loadModels() {
17081713 case sources.falai:
17091714 models = await loadFalaiModels();
17101715 break;
1716+ case sources.xai:
1717+ models = await loadXAIModels();
1718+ break;
17111719 }
17121720
17131721 for (const model of models) {
@@ -1760,6 +1768,12 @@ async function loadFalaiModels() {
17601768 return [];
17611769}
17621770
1771+async function loadXAIModels() {
1772+ return [
1773+ { value: 'grok-2-image-1212', text: 'grok-2-image-1212' },
1774+ ];
1775+}
1776+
17631777async function loadPollinationsModels() {
17641778 const result = await fetch('/api/sd/pollinations/models', {
17651779 method: 'POST',
@@ -2081,6 +2095,9 @@ async function loadSchedulers() {
20812095 case sources.falai:
20822096 schedulers = ['N/A'];
20832097 break;
2098+ case sources.xai:
2099+ schedulers = ['N/A'];
2100+ break;
20842101 }
20852102
20862103 for (const scheduler of schedulers) {
@@ -2166,6 +2183,12 @@ async function loadVaes() {
21662183 case sources.bfl:
21672184 vaes = ['N/A'];
21682185 break;
2186+ case sources.falai:
2187+ vaes = ['N/A'];
2188+ break;
2189+ case sources.xai:
2190+ vaes = ['N/A'];
2191+ break;
21692192 }
21702193
21712194 for (const vae of vaes) {
@@ -2735,6 +2758,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
27352758 case sources.falai:
27362759 result = await generateFalaiImage(prefixedPrompt, negativePrompt, signal);
27372760 break;
2761+ case sources.xai:
2762+ result = await generateXAIImage(prefixedPrompt, negativePrompt, signal);
2763+ break;
27382764 }
27392765
27402766 if (!result.data) {
@@ -3464,6 +3490,33 @@ async function generateBflImage(prompt, signal) {
34643490}
34653491
34663492/**
3493+ * Generates an image using the xAI API.
3494+ * @param {string} prompt The main instruction used to guide the image generation.
3495+ * @param {string} _negativePrompt Negative prompt is not used in this API
3496+ * @param {AbortSignal} signal An AbortSignal object that can be used to cancel the request.
3497+ * @returns {Promise<{format: string, data: string}>} A promise that resolves when the image generation and processing are complete.
3498+ */
3499+async function generateXAIImage(prompt, _negativePrompt, signal) {
3500+ const result = await fetch('/api/sd/xai/generate', {
3501+ method: 'POST',
3502+ headers: getRequestHeaders(),
3503+ signal: signal,
3504+ body: JSON.stringify({
3505+ prompt: prompt,
3506+ model: extension_settings.sd.model,
3507+ }),
3508+ });
3509+
3510+ if (result.ok) {
3511+ const data = await result.json();
3512+ return { format: 'jpg', data: data.image };
3513+ } else {
3514+ const text = await result.text();
3515+ throw new Error(text);
3516+ }
3517+}
3518+
3519+/**
34673520 * Generates an image using the FAL.AI API.
34683521 * @param {string} prompt - The main instruction used to guide the image generation.
34693522 * @param {string} negativePrompt - The negative prompt used to guide the image generation.
@@ -3782,6 +3835,8 @@ function isValidState() {
37823835 return secret_state[SECRET_KEYS.BFL];
37833836 case sources.falai:
37843837 return secret_state[SECRET_KEYS.FALAI];
3838+ case sources.xai:
3839+ return secret_state[SECRET_KEYS.XAI];
37853840 }
37863841}
37873842
public/scripts/extensions/stable-diffusion/settings.html+1 -0
@@ -52,6 +52,7 @@
5252 <option value="auto">Stable Diffusion Web UI (AUTOMATIC1111)</option>
5353 <option value="horde">Stable Horde</option>
5454 <option value="togetherai">TogetherAI</option>
55+ <option value="xai">xAI (Grok)</option>
5556 </select>
5657 <div data-sd-source="auto">
5758 <label for="sd_auto_url">SD Web UI URL</label>
public/scripts/openai.js+71 -2
@@ -184,6 +184,7 @@ export const chat_completion_sources = {
184184 ZEROONEAI: '01ai',
185185 NANOGPT: 'nanogpt',
186186 DEEPSEEK: 'deepseek',
187+ XAI: 'xai',
187188};
188189
189190const character_names_behavior = {
@@ -257,6 +258,7 @@ export const settingsToUpdate = {
257258 nanogpt_model: ['#model_nanogpt_select', 'nanogpt_model', false],
258259 deepseek_model: ['#model_deepseek_select', 'deepseek_model', false],
259260 zerooneai_model: ['#model_01ai_select', 'zerooneai_model', false],
261+ xai_model: ['#model_xai_select', 'xai_model', false],
260262 custom_model: ['#custom_model_id', 'custom_model', false],
261263 custom_url: ['#custom_api_url_text', 'custom_url', false],
262264 custom_include_body: ['#custom_include_body', 'custom_include_body', false],
@@ -345,6 +347,7 @@ const default_settings = {
345347 nanogpt_model: 'gpt-4o-mini',
346348 zerooneai_model: 'yi-large',
347349 deepseek_model: 'deepseek-chat',
350+ xai_model: 'grok-3-beta',
348351 custom_model: '',
349352 custom_url: '',
350353 custom_include_body: '',
@@ -425,6 +428,7 @@ const oai_settings = {
425428 nanogpt_model: 'gpt-4o-mini',
426429 zerooneai_model: 'yi-large',
427430 deepseek_model: 'deepseek-chat',
431+ xai_model: 'grok-3-beta',
428432 custom_model: '',
429433 custom_url: '',
430434 custom_include_body: '',
@@ -1644,6 +1648,8 @@ export function getChatCompletionModel(source = null) {
16441648 return oai_settings.nanogpt_model;
16451649 case chat_completion_sources.DEEPSEEK:
16461650 return oai_settings.deepseek_model;
1651+ case chat_completion_sources.XAI:
1652+ return oai_settings.xai_model;
16471653 default:
16481654 throw new Error(`Unknown chat completion source: ${activeSource}`);
16491655 }
@@ -1961,6 +1967,7 @@ async function sendOpenAIRequest(type, messages, signal) {
19611967 const is01AI = oai_settings.chat_completion_source == chat_completion_sources.ZEROONEAI;
19621968 const isNano = oai_settings.chat_completion_source == chat_completion_sources.NANOGPT;
19631969 const isDeepSeek = oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK;
1970+ const isXAI = oai_settings.chat_completion_source == chat_completion_sources.XAI;
19641971 const isTextCompletion = isOAI && textCompletionModels.includes(oai_settings.openai_model);
19651972 const isQuiet = type === 'quiet';
19661973 const isImpersonate = type === 'impersonate';
@@ -2033,7 +2040,7 @@ async function sendOpenAIRequest(type, messages, signal) {
20332040 }
20342041
20352042 // Add logprobs request (currently OpenAI only, max 5 on their side)
20362043 if (useLogprobs && (isOAI || isCustom || isDeepSeek || isXAI)) {
20372044 generate_data['logprobs'] = 5;
20382045 }
20392046
@@ -2152,7 +2159,18 @@ async function sendOpenAIRequest(type, messages, signal) {
21522159 }
21532160 }
21542161
2155- if ((isOAI || isOpenRouter || isMistral || isCustom || isCohere || isNano) && oai_settings.seed >= 0) {
2162+ if (isXAI) {
2163+ if (generate_data.model.includes('grok-3-mini')) {
2164+ delete generate_data.presence_penalty;
2165+ delete generate_data.frequency_penalty;
2166+ }
2167+ if (generate_data.model.includes('grok-vision')) {
2168+ delete generate_data.tools;
2169+ delete generate_data.tool_choice;
2170+ }
2171+ }
2172+
2173+ if ((isOAI || isOpenRouter || isMistral || isCustom || isCohere || isNano || isXAI) && oai_settings.seed >= 0) {
21562174 generate_data['seed'] = oai_settings.seed;
21572175 }
21582176
@@ -2278,6 +2296,11 @@ export function getStreamingReply(data, state, { chatCompletionSource = null, ov
22782296 state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content || '');
22792297 }
22802298 return data.choices?.[0]?.delta?.content || '';
2299+ } else if (chat_completion_source === chat_completion_sources.XAI) {
2300+ if (show_thoughts) {
2301+ state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content || '');
2302+ }
2303+ return data.choices?.[0]?.delta?.content || '';
22812304 } else if (chat_completion_source === chat_completion_sources.OPENROUTER) {
22822305 if (show_thoughts) {
22832306 state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning || '');
@@ -2310,6 +2333,7 @@ function parseChatCompletionLogprobs(data) {
23102333 switch (oai_settings.chat_completion_source) {
23112334 case chat_completion_sources.OPENAI:
23122335 case chat_completion_sources.DEEPSEEK:
2336+ case chat_completion_sources.XAI:
23132337 case chat_completion_sources.CUSTOM:
23142338 if (!data.choices?.length) {
23152339 return null;
@@ -3231,6 +3255,7 @@ function loadOpenAISettings(data, settings) {
32313255 oai_settings.nanogpt_model = settings.nanogpt_model ?? default_settings.nanogpt_model;
32323256 oai_settings.deepseek_model = settings.deepseek_model ?? default_settings.deepseek_model;
32333257 oai_settings.zerooneai_model = settings.zerooneai_model ?? default_settings.zerooneai_model;
3258+ oai_settings.xai_model = settings.xai_model ?? default_settings.xai_model;
32343259 oai_settings.custom_model = settings.custom_model ?? default_settings.custom_model;
32353260 oai_settings.custom_url = settings.custom_url ?? default_settings.custom_url;
32363261 oai_settings.custom_include_body = settings.custom_include_body ?? default_settings.custom_include_body;
@@ -3316,6 +3341,8 @@ function loadOpenAISettings(data, settings) {
33163341 $('#model_deepseek_select').val(oai_settings.deepseek_model);
33173342 $(`#model_deepseek_select option[value="${oai_settings.deepseek_model}"`).prop('selected', true);
33183343 $('#model_01ai_select').val(oai_settings.zerooneai_model);
3344+ $('#model_xai_select').val(oai_settings.xai_model);
3345+ $(`#model_xai_select option[value="${oai_settings.xai_model}"`).attr('selected', true);
33193346 $('#custom_model_id').val(oai_settings.custom_model);
33203347 $('#custom_api_url_text').val(oai_settings.custom_url);
33213348 $('#openai_max_context').val(oai_settings.openai_max_context);
@@ -4386,6 +4413,11 @@ async function onModelChange() {
43864413 $('#custom_model_id').val(value).trigger('input');
43874414 }
43884415
4416+ if ($(this).is('#model_xai_select')) {
4417+ console.log('XAI model changed to', value);
4418+ oai_settings.xai_model = value;
4419+ }
4420+
43894421 if (oai_settings.chat_completion_source == chat_completion_sources.SCALE) {
43904422 if (oai_settings.max_context_unlocked) {
43914423 $('#openai_max_context').attr('max', unlocked_max);
@@ -4641,6 +4673,22 @@ async function onModelChange() {
46414673 $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
46424674 }
46434675
4676+ if (oai_settings.chat_completion_source === chat_completion_sources.XAI) {
4677+ if (oai_settings.max_context_unlocked) {
4678+ $('#openai_max_context').attr('max', unlocked_max);
4679+ } else if (oai_settings.xai_model.includes('grok-2-vision')) {
4680+ $('#openai_max_context').attr('max', max_32k);
4681+ } else if (oai_settings.xai_model.includes('grok-vision')) {
4682+ $('#openai_max_context').attr('max', max_8k);
4683+ } else {
4684+ $('#openai_max_context').attr('max', max_128k);
4685+ }
4686+
4687+ oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
4688+ $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
4689+ $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
4690+ }
4691+
46444692 if (oai_settings.chat_completion_source === chat_completion_sources.COHERE) {
46454693 oai_settings.pres_pen_openai = Math.min(Math.max(0, oai_settings.pres_pen_openai), 1);
46464694 $('#pres_pen_openai').attr('max', 1).attr('min', 0).val(oai_settings.pres_pen_openai).trigger('input');
@@ -4879,6 +4927,19 @@ async function onConnectButtonClick(e) {
48794927 }
48804928 }
48814929
4930+ if (oai_settings.chat_completion_source === chat_completion_sources.XAI) {
4931+ const api_key_xai = String($('#api_key_xai').val()).trim();
4932+
4933+ if (api_key_xai.length) {
4934+ await writeSecret(SECRET_KEYS.XAI, api_key_xai);
4935+ }
4936+
4937+ if (!secret_state[SECRET_KEYS.XAI]) {
4938+ console.log('No secret key saved for XAI');
4939+ return;
4940+ }
4941+ }
4942+
48824943 startStatusLoading();
48834944 saveSettingsDebounced();
48844945 await getStatusOpen();
@@ -4935,6 +4996,9 @@ function toggleChatCompletionForms() {
49354996 else if (oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK) {
49364997 $('#model_deepseek_select').trigger('change');
49374998 }
4999+ else if (oai_settings.chat_completion_source == chat_completion_sources.XAI) {
5000+ $('#model_xai_select').trigger('change');
5001+ }
49385002 $('[data-source]').each(function () {
49395003 const validSources = $(this).data('source').split(',');
49405004 $(this).toggle(validSources.includes(oai_settings.chat_completion_source));
@@ -5073,6 +5137,8 @@ export function isImageInliningSupported() {
50735137 'pixtral-large-2411',
50745138 'c4ai-aya-vision-8b',
50755139 'c4ai-aya-vision-32b',
5140+ 'grok-2-vision',
5141+ 'grok-vision',
50765142 ];
50775143
50785144 switch (oai_settings.chat_completion_source) {
@@ -5092,6 +5158,8 @@ export function isImageInliningSupported() {
50925158 return visionSupportedModels.some(model => oai_settings.mistralai_model.includes(model));
50935159 case chat_completion_sources.COHERE:
50945160 return visionSupportedModels.some(model => oai_settings.cohere_model.includes(model));
5161+ case chat_completion_sources.XAI:
5162+ return visionSupportedModels.some(model => oai_settings.xai_model.includes(model));
50955163 default:
50965164 return false;
50975165 }
@@ -5688,6 +5756,7 @@ export function initOpenAI() {
56885756 $('#model_deepseek_select').on('change', onModelChange);
56895757 $('#model_01ai_select').on('change', onModelChange);
56905758 $('#model_custom_select').on('change', onModelChange);
5759+ $('#model_xai_select').on('change', onModelChange);
56915760 $('#settings_preset_openai').on('change', onSettingsPresetChange);
56925761 $('#new_oai_preset').on('click', onNewPresetClick);
56935762 $('#delete_oai_preset').on('click', onDeletePresetClick);
public/scripts/reasoning.js+2 -0
@@ -109,6 +109,8 @@ export function extractReasoningFromData(data, {
109109 switch (chatCompletionSource ?? oai_settings.chat_completion_source) {
110110 case chat_completion_sources.DEEPSEEK:
111111 return data?.choices?.[0]?.message?.reasoning_content ?? '';
112+ case chat_completion_sources.XAI:
113+ return data?.choices?.[0]?.message?.reasoning_content ?? '';
112114 case chat_completion_sources.OPENROUTER:
113115 return data?.choices?.[0]?.message?.reasoning ?? '';
114116 case chat_completion_sources.MAKERSUITE:
public/scripts/secrets.js+2 -0
@@ -42,6 +42,7 @@ export const SECRET_KEYS = {
4242 DEEPSEEK: 'api_key_deepseek',
4343 SERPER: 'api_key_serper',
4444 FALAI: 'api_key_falai',
45+ XAI: 'api_key_xai',
4546};
4647
4748const INPUT_MAP = {
@@ -76,6 +77,7 @@ const INPUT_MAP = {
7677 [SECRET_KEYS.NANOGPT]: '#api_key_nanogpt',
7778 [SECRET_KEYS.GENERIC]: '#api_key_generic',
7879 [SECRET_KEYS.DEEPSEEK]: '#api_key_deepseek',
80+ [SECRET_KEYS.XAI]: '#api_key_xai',
7981};
8082
8183async function clearSecret() {
public/scripts/slash-commands.js+1 -0
@@ -3942,6 +3942,7 @@ function getModelOptions(quiet) {
39423942 { id: 'model_nanogpt_select', api: 'openai', type: chat_completion_sources.NANOGPT },
39433943 { id: 'model_01ai_select', api: 'openai', type: chat_completion_sources.ZEROONEAI },
39443944 { id: 'model_deepseek_select', api: 'openai', type: chat_completion_sources.DEEPSEEK },
3945+ { id: 'model_xai_select', api: 'openai', type: chat_completion_sources.XAI },
39453946 { id: 'model_novel_select', api: 'novel', type: null },
39463947 { id: 'horde_model', api: 'koboldhorde', type: null },
39473948 ];
public/scripts/tool-calling.js+1 -0
@@ -586,6 +586,7 @@ export class ToolManager {
586586 chat_completion_sources.DEEPSEEK,
587587 chat_completion_sources.MAKERSUITE,
588588 chat_completion_sources.AI21,
589+ chat_completion_sources.XAI,
589590 ];
590591 return supportedSources.includes(oai_settings.chat_completion_source);
591592 }
src/constants.js+1 -0
@@ -176,6 +176,7 @@ export const CHAT_COMPLETION_SOURCES = {
176176 ZEROONEAI: '01ai',
177177 NANOGPT: 'nanogpt',
178178 DEEPSEEK: 'deepseek',
179+ XAI: 'xai',
179180};
180181
181182/**
src/endpoints/backends/chat-completions.js+17 -0
@@ -23,6 +23,7 @@ import {
2323 convertCohereMessages,
2424 convertMistralMessages,
2525 convertAI21Messages,
26+ convertXAIMessages,
2627 mergeMessages,
2728 cachingAtDepthForOpenRouterClaude,
2829 cachingAtDepthForClaude,
@@ -53,6 +54,7 @@ const API_01AI = 'https://api.lingyiwanwu.com/v1';
5354const API_AI21 = 'https://api.ai21.com/studio/v1';
5455const API_NANOGPT = 'https://nano-gpt.com/api/v1';
5556const API_DEEPSEEK = 'https://api.deepseek.com/beta';
57+const API_XAI = 'https://api.x.ai/v1';
5658
5759/**
5860 * Applies a post-processing step to the generated messages.
@@ -872,6 +874,9 @@ router.post('/status', async function (request, response_getstatus_openai) {
872874 api_url = new URL(request.body.reverse_proxy || API_DEEPSEEK.replace('/beta', ''));
873875 api_key_openai = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK);
874876 headers = {};
877+ } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.XAI) {
878+ api_url = API_XAI;
879+ api_key_openai = readSecret(request.user.directories, SECRET_KEYS.XAI);
875880 } else {
876881 console.warn('This chat completion source is not supported yet.');
877882 return response_getstatus_openai.status(400).send({ error: true });
@@ -1150,6 +1155,12 @@ router.post('/generate', function (request, response) {
11501155 apiKey = readSecret(request.user.directories, SECRET_KEYS.ZEROONEAI);
11511156 headers = {};
11521157 bodyParams = {};
1158+ } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.XAI) {
1159+ apiUrl = API_XAI;
1160+ apiKey = readSecret(request.user.directories, SECRET_KEYS.XAI);
1161+ headers = {};
1162+ bodyParams = {};
1163+ request.body.messages = convertXAIMessages(request.body.messages, getPromptNames(request));
11531164 } else {
11541165 console.warn('This chat completion source is not supported yet.');
11551166 return response.status(400).send({ error: true });
@@ -1162,6 +1173,12 @@ router.post('/generate', function (request, response) {
11621173 }
11631174 }
11641175
1176+ if ([CHAT_COMPLETION_SOURCES.XAI].includes(request.body.chat_completion_source)) {
1177+ if (['grok-3-mini-beta', 'grok-3-mini-fast-beta'].includes(request.body.model)) {
1178+ bodyParams['reasoning_effort'] = request.body.reasoning_effort === 'high' ? 'high' : 'low';
1179+ }
1180+ }
1181+
11651182 if (!apiKey && !request.body.reverse_proxy && request.body.chat_completion_source !== CHAT_COMPLETION_SOURCES.CUSTOM) {
11661183 console.warn('OpenAI API key is missing.');
11671184 return response.status(400).send({ error: true });
src/endpoints/openai.js+8 -0
@@ -65,6 +65,10 @@ router.post('/caption-image', async (request, response) => {
6565 key = readSecret(request.user.directories, SECRET_KEYS.COHERE);
6666 }
6767
68+ if (request.body.api === 'xai') {
69+ key = readSecret(request.user.directories, SECRET_KEYS.XAI);
70+ }
71+
6872 if (!key && !request.body.reverse_proxy && ['custom', 'ooba', 'koboldcpp', 'vllm'].includes(request.body.api) === false) {
6973 console.warn('No key found for API', request.body.api);
7074 return response.sendStatus(400);
@@ -134,6 +138,10 @@ router.post('/caption-image', async (request, response) => {
134138 apiUrl = 'https://api.cohere.ai/v2/chat';
135139 }
136140
141+ if (request.body.api === 'xai') {
142+ apiUrl = 'https://api.x.ai/v1/chat/completions';
143+ }
144+
137145 if (request.body.api === 'ooba') {
138146 apiUrl = `${trimV1(request.body.server_url)}/v1/chat/completions`;
139147 const imgMessage = body.messages.pop();
src/endpoints/secrets.js+1 -0
@@ -52,6 +52,7 @@ export const SECRET_KEYS = {
5252 GENERIC: 'api_key_generic',
5353 DEEPSEEK: 'api_key_deepseek',
5454 SERPER: 'api_key_serper',
55+ XAI: 'api_key_xai',
5556};
5657
5758// These are the keys that are safe to expose, even if allowKeysExposure is false
src/endpoints/stable-diffusion.js+54 -2
@@ -627,8 +627,8 @@ together.post('/models', async (request, response) => {
627627 }
628628
629629 const models = data
630630 .filter(x => x.display_typetype === 'image')
631631 .map(x => ({ value: x.nameid, text: x.display_name }));
632632
633633 return response.send(models);
634634 } catch (error) {
@@ -1246,6 +1246,7 @@ falai.post('/generate', async (request, response) => {
12461246 'Authorization': `Key ${key}`,
12471247 },
12481248 });
1249+ /** @type {any} */
12491250 const resultData = await resultFetch.json();
12501251
12511252 if (resultData.detail !== null && resultData.detail !== undefined) {
@@ -1271,6 +1272,56 @@ falai.post('/generate', async (request, response) => {
12711272 }
12721273});
12731274
1275+const xai = express.Router();
1276+
1277+xai.post('/generate', async (request, response) => {
1278+ try {
1279+ const key = readSecret(request.user.directories, SECRET_KEYS.XAI);
1280+
1281+ if (!key) {
1282+ console.warn('xAI key not found.');
1283+ return response.sendStatus(400);
1284+ }
1285+
1286+ const requestBody = {
1287+ prompt: request.body.prompt,
1288+ model: request.body.model,
1289+ response_format: 'b64_json',
1290+ };
1291+
1292+ console.debug('xAI request:', requestBody);
1293+
1294+ const result = await fetch('https://api.x.ai/v1/images/generations', {
1295+ method: 'POST',
1296+ body: JSON.stringify(requestBody),
1297+ headers: {
1298+ 'Content-Type': 'application/json',
1299+ 'Authorization': `Bearer ${key}`,
1300+ },
1301+ });
1302+
1303+ if (!result.ok) {
1304+ const text = await result.text();
1305+ console.warn('xAI returned an error.', text);
1306+ return response.sendStatus(500);
1307+ }
1308+
1309+ /** @type {any} */
1310+ const data = await result.json();
1311+
1312+ const image = data?.data?.[0]?.b64_json;
1313+ if (!image) {
1314+ console.warn('xAI returned invalid data.');
1315+ return response.sendStatus(500);
1316+ }
1317+
1318+ return response.send({ image });
1319+ } catch (error) {
1320+ console.error('Error communicating with xAI', error);
1321+ return response.sendStatus(500);
1322+ }
1323+});
1324+
12741325router.use('/comfy', comfy);
12751326router.use('/together', together);
12761327router.use('/drawthings', drawthings);
@@ -1280,3 +1331,4 @@ router.use('/huggingface', huggingface);
12801331router.use('/nanogpt', nanogpt);
12811332router.use('/bfl', bfl);
12821333router.use('/falai', falai);
1334+router.use('/xai', xai);
src/prompt-converters.js+37 -0
@@ -680,6 +680,43 @@ export function convertMistralMessages(messages, names) {
680680}
681681
682682/**
683+ * Convert a prompt from the messages objects to the format used by xAI.
684+ * @param {object[]} messages Array of messages
685+ * @param {PromptNames} names Prompt names
686+ * @returns {object[]} Prompt for xAI
687+ */
688+export function convertXAIMessages(messages, names) {
689+ if (!Array.isArray(messages)) {
690+ return [];
691+ }
692+
693+ messages.forEach(msg => {
694+ if (!msg.name || msg.role === 'user') {
695+ return;
696+ }
697+
698+ const needsCharNamePrefix = [
699+ { role: 'assistant', condition: names.charName && !msg.content.startsWith(`${names.charName}: `) && !names.startsWithGroupName(msg.content) },
700+ { role: 'system', name: 'example_assistant', condition: names.charName && !msg.content.startsWith(`${names.charName}: `) && !names.startsWithGroupName(msg.content) },
701+ { role: 'system', name: 'example_user', condition: names.userName && !msg.content.startsWith(`${names.userName}: `) },
702+ ];
703+
704+ const matchingRule = needsCharNamePrefix.find(rule =>
705+ msg.role === rule.role && (!rule.name || msg.name === rule.name) && rule.condition,
706+ );
707+
708+ if (matchingRule) {
709+ const prefix = msg.role === 'system' && msg.name === 'example_user' ? names.userName : names.charName;
710+ msg.content = `${prefix}: ${msg.content}`;
711+ }
712+
713+ delete msg.name;
714+ });
715+
716+ return messages;
717+}
718+
719+/**
683720 * Merge messages with the same consecutive role, removing names if they exist.
684721 * @param {any[]} messages Messages to merge
685722 * @param {PromptNames} names Prompt names