Merge pull request #3852 from subzero5544/xAI-grok-reverse-proxy-testing Adding reverse proxy support to xai chat completion
Signed| @@ -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."> |
| @@ -2039,7 +2039,7 @@ async function sendOpenAIRequest(type, messages, signal) { | |||
| 2039 | } | 2039 | } |
| 2040 | 2040 | ||
| 2041 | // Proxy is only supported for Claude, OpenAI, Mistral, and Google MakerSuite | 2041 | // 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 | }; |
| 3547 | 3547 | ||
| 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 | } |
| 3551 | 3551 | ||
| @@ -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 | } |
| 4945 | 4945 | ||
| 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 | } |
| @@ -830,6 +830,100 @@ async function sendDeepSeekRequest(request, response) { | |||
| 830 | } | 830 | } |
| 831 | } | 831 | } |
| 832 | 832 | ||
| 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 | } | ||
| 833 | 927 | ||
| 834 | export const router = express.Router(); | 928 | export const router = express.Router(); |
| 835 | 929 | ||
| @@ -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 | } |
| 1048 | 1144 | ||
| 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 | } |
| 1175 | 1265 | ||
| 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 }); |