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 @@
2762 <option value="xai">xAI (Grok)</option>2762 <option value="xai">xAI (Grok)</option>
2763 </optgroup>2763 </optgroup>
2764 </select>2764 </select>
2765 <div class="inline-drawer wide100p" data-source="openai,claude,mistralai,makersuite,deepseek">2765 <div class="inline-drawer wide100p" data-source="openai,claude,mistralai,makersuite,deepseek,xai">
2766 <div class="inline-drawer-toggle inline-drawer-header">2766 <div class="inline-drawer-toggle inline-drawer-header">
2767 <b data-i18n="Reverse Proxy">Reverse Proxy</b>2767 <b data-i18n="Reverse Proxy">Reverse Proxy</b>
2768 <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>2768 <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
@@ -2825,7 +2825,7 @@
2825 </div>2825 </div>
2826 </div>2826 </div>
2827 </div>2827 </div>
2828 <div id="ReverseProxyWarningMessage" data-source="openai,claude,mistralai,makersuite,deepseek">2828 <div id="ReverseProxyWarningMessage" data-source="openai,claude,mistralai,makersuite,deepseek,xai">
2829 <div class="reverse_proxy_warning">2829 <div class="reverse_proxy_warning">
2830 <b>2830 <b>
2831 <div data-i18n="Using a proxy that you're not running yourself is a risk to your data privacy.">2831 <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) {
2039 }2039 }
20402040
2041 // Proxy is only supported for Claude, OpenAI, Mistral, and Google MakerSuite2041 // Proxy is only supported for Claude, OpenAI, Mistral, and Google MakerSuite
2042 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].includes(oai_settings.chat_completion_source)) {2042 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)) {
2043 await validateReverseProxy();2043 await validateReverseProxy();
2044 generate_data['reverse_proxy'] = oai_settings.reverse_proxy;2044 generate_data['reverse_proxy'] = oai_settings.reverse_proxy;
2045 generate_data['proxy_password'] = oai_settings.proxy_password;2045 generate_data['proxy_password'] = oai_settings.proxy_password;
@@ -3545,7 +3545,7 @@ async function getStatusOpen() {
3545 chat_completion_source: oai_settings.chat_completion_source,3545 chat_completion_source: oai_settings.chat_completion_source,
3546 };3546 };
35473547
3548 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].includes(oai_settings.chat_completion_source)) {3548 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)) {
3549 await validateReverseProxy();3549 await validateReverseProxy();
3550 }3550 }
35513551
@@ -4943,7 +4943,7 @@ async function onConnectButtonClick(e) {
4943 await writeSecret(SECRET_KEYS.XAI, api_key_xai);4943 await writeSecret(SECRET_KEYS.XAI, api_key_xai);
4944 }4944 }
49454945
4946 if (!secret_state[SECRET_KEYS.XAI]) {4946 if (!secret_state[SECRET_KEYS.XAI] && !oai_settings.reverse_proxy) {
4947 console.log('No secret key saved for XAI');4947 console.log('No secret key saved for XAI');
4948 return;4948 return;
4949 }4949 }
src/endpoints/backends/chat-completions.js+98 -14
@@ -830,6 +830,100 @@ async function sendDeepSeekRequest(request, response) {
830 }830 }
831}831}
832832
833/**
834 * Sends a request to XAI API.
835 * @param {express.Request} request Express request
836 * @param {express.Response} response Express response
837 */
838async 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
834export const router = express.Router();928export const router = express.Router();
835929
@@ -875,8 +969,9 @@ router.post('/status', async function (request, response_getstatus_openai) {
875 api_key_openai = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK);969 api_key_openai = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK);
876 headers = {};970 headers = {};
877 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.XAI) {971 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.XAI) {
878 api_url = API_XAI;972 api_url = new URL(request.body.reverse_proxy || API_XAI);
879 api_key_openai = readSecret(request.user.directories, SECRET_KEYS.XAI);973 api_key_openai = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.XAI);
974 headers = {};
880 } else {975 } else {
881 console.warn('This chat completion source is not supported yet.');976 console.warn('This chat completion source is not supported yet.');
882 return response_getstatus_openai.status(400).send({ error: true });977 return response_getstatus_openai.status(400).send({ error: true });
@@ -1044,6 +1139,7 @@ router.post('/generate', function (request, response) {
1044 case CHAT_COMPLETION_SOURCES.MISTRALAI: return sendMistralAIRequest(request, response);1139 case CHAT_COMPLETION_SOURCES.MISTRALAI: return sendMistralAIRequest(request, response);
1045 case CHAT_COMPLETION_SOURCES.COHERE: return sendCohereRequest(request, response);1140 case CHAT_COMPLETION_SOURCES.COHERE: return sendCohereRequest(request, response);
1046 case CHAT_COMPLETION_SOURCES.DEEPSEEK: return sendDeepSeekRequest(request, response);1141 case CHAT_COMPLETION_SOURCES.DEEPSEEK: return sendDeepSeekRequest(request, response);
1142 case CHAT_COMPLETION_SOURCES.XAI: return sendXaiRequest(request, response);
1047 }1143 }
10481144
1049 let apiUrl;1145 let apiUrl;
@@ -1155,12 +1251,6 @@ router.post('/generate', function (request, response) {
1155 apiKey = readSecret(request.user.directories, SECRET_KEYS.ZEROONEAI);1251 apiKey = readSecret(request.user.directories, SECRET_KEYS.ZEROONEAI);
1156 headers = {};1252 headers = {};
1157 bodyParams = {};1253 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));
1164 } else {1254 } else {
1165 console.warn('This chat completion source is not supported yet.');1255 console.warn('This chat completion source is not supported yet.');
1166 return response.status(400).send({ error: true });1256 return response.status(400).send({ error: true });
@@ -1173,12 +1263,6 @@ router.post('/generate', function (request, response) {
1173 }1263 }
1174 }1264 }
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
1182 if (!apiKey && !request.body.reverse_proxy && request.body.chat_completion_source !== CHAT_COMPLETION_SOURCES.CUSTOM) {1266 if (!apiKey && !request.body.reverse_proxy && request.body.chat_completion_source !== CHAT_COMPLETION_SOURCES.CUSTOM) {
1183 console.warn('OpenAI API key is missing.');1267 console.warn('OpenAI API key is missing.');
1184 return response.status(400).send({ error: true });1268 return response.status(400).send({ error: true });