Merge pull request #3852 from subzero5544/xAI-grok-reverse-proxy-testing Adding reverse proxy support to xai chat completion

c3717ff06a90821cc252efad3d70aa06b5125c75

Subwolf <jeraran23@gmail.com>

Signed
3 files changed, +103 -19Showing whitespace changes
public/index.html+2 -2
@@ -2762,7 +2762,7 @@
27622762 <option value="xai">xAI (Grok)</option>
27632763 </optgroup>
27642764 </select>
27652765 <div class="inline-drawer wide100p" data-source="openai,claude,mistralai,makersuite,deepseek,xai">
27662766 <div class="inline-drawer-toggle inline-drawer-header">
27672767 <b data-i18n="Reverse Proxy">Reverse Proxy</b>
27682768 <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
@@ -2825,7 +2825,7 @@
28252825 </div>
28262826 </div>
28272827 </div>
28282828 <div id="ReverseProxyWarningMessage" data-source="openai,claude,mistralai,makersuite,deepseek,xai">
28292829 <div class="reverse_proxy_warning">
28302830 <b>
28312831 <div data-i18n="Using a proxy that you're not running yourself is a risk to your data privacy.">
public/scripts/openai.js+3 -3
@@ -2039,7 +2039,7 @@ async function sendOpenAIRequest(type, messages, signal) {
20392039 }
20402040
20412041 // Proxy is only supported for Claude, OpenAI, Mistral, and Google MakerSuite
20422042 if (oai_settings.reverse_proxy && [chat_completion_sources.CLAUDE, chat_completion_sources.OPENAI, chat_completion_sources.MISTRALAI, chat_completion_sources.MAKERSUITE, chat_completion_sources.DEEPSEEK, chat_completion_sources.XAI].includes(oai_settings.chat_completion_source)) {
20432043 await validateReverseProxy();
20442044 generate_data['reverse_proxy'] = oai_settings.reverse_proxy;
20452045 generate_data['proxy_password'] = oai_settings.proxy_password;
@@ -3545,7 +3545,7 @@ async function getStatusOpen() {
35453545 chat_completion_source: oai_settings.chat_completion_source,
35463546 };
35473547
35483548 if (oai_settings.reverse_proxy && [chat_completion_sources.CLAUDE, chat_completion_sources.OPENAI, chat_completion_sources.MISTRALAI, chat_completion_sources.MAKERSUITE, chat_completion_sources.DEEPSEEK, chat_completion_sources.XAI].includes(oai_settings.chat_completion_source)) {
35493549 await validateReverseProxy();
35503550 }
35513551
@@ -4943,7 +4943,7 @@ async function onConnectButtonClick(e) {
49434943 await writeSecret(SECRET_KEYS.XAI, api_key_xai);
49444944 }
49454945
49464946 if (!secret_state[SECRET_KEYS.XAI] && !oai_settings.reverse_proxy) {
49474947 console.log('No secret key saved for XAI');
49484948 return;
49494949 }
src/endpoints/backends/chat-completions.js+98 -14
@@ -830,6 +830,100 @@ async function sendDeepSeekRequest(request, response) {
830830 }
831831}
832832
833+/**
834+ * Sends a request to XAI API.
835+ * @param {express.Request} request Express request
836+ * @param {express.Response} response Express response
837+ */
838+async function sendXaiRequest(request, response) {
839+ const apiUrl = new URL(request.body.reverse_proxy || API_XAI).toString();
840+ const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.XAI);
841+
842+ if (!apiKey && !request.body.reverse_proxy) {
843+ console.warn('xAI API key is missing.');
844+ return response.status(400).send({ error: true });
845+ }
846+
847+ const controller = new AbortController();
848+ request.socket.removeAllListeners('close');
849+ request.socket.on('close', function () {
850+ controller.abort();
851+ });
852+
853+ try {
854+ let bodyParams = {};
855+
856+ if (request.body.logprobs > 0) {
857+ bodyParams['top_logprobs'] = request.body.logprobs;
858+ bodyParams['logprobs'] = true;
859+ }
860+
861+ if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
862+ bodyParams['tools'] = request.body.tools;
863+ bodyParams['tool_choice'] = request.body.tool_choice;
864+ }
865+
866+ if (Array.isArray(request.body.stop) && request.body.stop.length > 0) {
867+ bodyParams['stop'] = request.body.stop;
868+ }
869+
870+ if (['grok-3-mini-beta', 'grok-3-mini-fast-beta'].includes(request.body.model)) {
871+ bodyParams['reasoning_effort'] = request.body.reasoning_effort === 'high' ? 'high' : 'low';
872+ }
873+
874+ const processedMessages = request.body.messages = convertXAIMessages(request.body.messages, getPromptNames(request));
875+
876+ const requestBody = {
877+ 'messages': processedMessages,
878+ 'model': request.body.model,
879+ 'temperature': request.body.temperature,
880+ 'max_tokens': request.body.max_tokens,
881+ 'max_completion_tokens': request.body.max_completion_tokens,
882+ 'stream': request.body.stream,
883+ 'presence_penalty': request.body.presence_penalty,
884+ 'frequency_penalty': request.body.frequency_penalty,
885+ 'top_p': request.body.top_p,
886+ 'seed': request.body.seed,
887+ 'n': request.body.n,
888+ ...bodyParams,
889+ };
890+
891+ const config = {
892+ method: 'POST',
893+ headers: {
894+ 'Content-Type': 'application/json',
895+ 'Authorization': 'Bearer ' + apiKey,
896+ },
897+ body: JSON.stringify(requestBody),
898+ signal: controller.signal,
899+ };
900+
901+ console.debug('xAI request:', requestBody);
902+
903+ const generateResponse = await fetch(apiUrl + '/chat/completions', config);
904+
905+ if (request.body.stream) {
906+ forwardFetchResponse(generateResponse, response);
907+ } else {
908+ if (!generateResponse.ok) {
909+ const errorText = await generateResponse.text();
910+ console.warn(`xAI API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);
911+ const errorJson = tryParse(errorText) ?? { error: true };
912+ return response.status(500).send(errorJson);
913+ }
914+ const generateResponseJson = await generateResponse.json();
915+ console.debug('xAI response:', generateResponseJson);
916+ return response.send(generateResponseJson);
917+ }
918+ } catch (error) {
919+ console.error('Error communicating with xAI API: ', error);
920+ if (!response.headersSent) {
921+ response.send({ error: true });
922+ } else {
923+ response.end();
924+ }
925+ }
926+}
833927
834928export const router = express.Router();
835929
@@ -875,8 +969,9 @@ router.post('/status', async function (request, response_getstatus_openai) {
875969 api_key_openai = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK);
876970 headers = {};
877971 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.XAI) {
878972 api_url = new URL(request.body.reverse_proxy || API_XAI);
879973 api_key_openai = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.XAI);
974+ headers = {};
880975 } else {
881976 console.warn('This chat completion source is not supported yet.');
882977 return response_getstatus_openai.status(400).send({ error: true });
@@ -1044,6 +1139,7 @@ router.post('/generate', function (request, response) {
10441139 case CHAT_COMPLETION_SOURCES.MISTRALAI: return sendMistralAIRequest(request, response);
10451140 case CHAT_COMPLETION_SOURCES.COHERE: return sendCohereRequest(request, response);
10461141 case CHAT_COMPLETION_SOURCES.DEEPSEEK: return sendDeepSeekRequest(request, response);
1142+ case CHAT_COMPLETION_SOURCES.XAI: return sendXaiRequest(request, response);
10471143 }
10481144
10491145 let apiUrl;
@@ -1155,12 +1251,6 @@ router.post('/generate', function (request, response) {
11551251 apiKey = readSecret(request.user.directories, SECRET_KEYS.ZEROONEAI);
11561252 headers = {};
11571253 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));
11641254 } else {
11651255 console.warn('This chat completion source is not supported yet.');
11661256 return response.status(400).send({ error: true });
@@ -1173,12 +1263,6 @@ router.post('/generate', function (request, response) {
11731263 }
11741264 }
11751265
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-
11821266 if (!apiKey && !request.body.reverse_proxy && request.body.chat_completion_source !== CHAT_COMPLETION_SOURCES.CUSTOM) {
11831267 console.warn('OpenAI API key is missing.');
11841268 return response.status(400).send({ error: true });