{"repo": "adventchilde/resumedemo", "files": {"src/endpoints/backends/chat-completions.js": {"484239c713c6c17fd37296d9c51bd2eef384dd1c": {"t": 1785746692.6772077, "n": 107141, "h": "a1b714129eb60c76", "c": "import process from 'node:process';\nimport util from 'node:util';\nimport express from 'express';\nimport fetch from 'node-fetch';\nimport urlJoin from 'url-join';\n\nimport {\n AIMLAPI_HEADERS,\n AZURE_OPENAI_KEYS,\n CHAT_COMPLETION_SOURCES,\n GEMINI_SAFETY,\n OPENAI_REASONING_EFFORT_MAP,\n OPENAI_REASONING_EFFORT_MODELS,\n OPENAI_VERBOSITY_MODELS,\n OPENROUTER_HEADERS,\n VERTEX_SAFETY,\n ZAI_ENDPOINT,\n} from '../../constants.js';\nimport {\n forwardFetchResponse,\n getConfigValue,\n tryParse,\n uuidv4,\n mergeObjectWithYaml,\n excludeKeysByYaml,\n color,\n trimTrailingSlash,\n flattenSchema,\n} from '../../util.js';\nimport {\n convertClaudeMessages,\n convertGooglePrompt,\n convertTextCompletionPrompt,\n convertCohereMessages,\n convertMistralMessages,\n convertAI21Messages,\n convertXAIMessages,\n cachingAtDepthForOpenRouterClaude,\n cachingAtDepthForClaude,\n getPromptNames,\n calculateClaudeBudgetTokens,\n calculateGoogleBudgetTokens,\n postProcessPrompt,\n PROMPT_PROCESSING_TYPE,\n addAssistantPrefix,\n embedOpenRouterMedia,\n addReasoningContentToToolCalls,\n cachingSystemPromptForOpenRouter,\n addOpenRouterSignatures,\n} from '../../prompt-converters.js';\n\nimport { readSecret, SECRET_KEYS } from '../secrets.js';\nimport {\n getTokenizerModel,\n getSentencepiceTokenizer,\n getTiktokenTokenizer,\n sentencepieceTokenizers,\n TEXT_COMPLETION_MODELS,\n webTokenizers,\n getWebTokenizer,\n} from '../tokenizers.js';\nimport { getVertexAIAuth, getProjectIdFromServiceAccount } from '../google.js';\n\nconst API_OPENAI = 'https://api.openai.com/v1';\nconst API_CLAUDE = 'https://api.anthropic.com/v1';\nconst API_MISTRAL = 'https://api.mistral.ai/v1';\nconst API_COHERE_V1 = 'https://api.cohere.ai/v1';\nconst API_COHERE_V2 = 'https://api.cohere.ai/v2';\nconst API_PERPLEXITY = 'https://api.perplexity.ai';\nconst API_GROQ = 'https://api.groq.com/openai/v1';\nconst API_MAKERSUITE = 'https://generativelanguage.googleapis.com';\nconst API_VERTEX_AI = 'https://us-central1-aiplatform.googleapis.com';\nconst API_AI21 = 'https://api.ai21.com/studio/v1';\nconst API_CHUTES = 'https://llm.chutes.ai/v1';\nconst API_ELECTRONHUB = 'https://api.electronhub.ai/v1';\nconst API_NANOGPT = 'https://nano-gpt.com/api/v1';\nconst API_DEEPSEEK = 'https://api.deepseek.com/beta';\nconst API_XAI = 'https://api.x.ai/v1';\nconst API_AIMLAPI = 'https://api.aimlapi.com/v1';\nconst API_POLLINATIONS = 'https://text.pollinations.ai/openai';\nconst API_MOONSHOT = 'https://api.moonshot.ai/v1';\nconst API_FIREWORKS = 'https://api.fireworks.ai/inference/v1';\nconst API_COMETAPI = 'https://api.cometapi.com/v1';\nconst API_ZAI_COMMON = 'https://api.z.ai/api/paas/v4';\nconst API_ZAI_CODING = 'https://api.z.ai/api/coding/paas/v4';\nconst API_SILICONFLOW = 'https://api.siliconflow.com/v1';\nconst API_OPENROUTER = 'https://openrouter.ai/api/v1';\n\n/**\n * Module-scoped Claude caching configuration values.\n */\nconst cacheTTL = getConfigValue('claude.extendedTTL', false, 'boolean') ? '1h' : '5m';\nconst enableSystemPromptCache = getConfigValue('claude.enableSystemPromptCache', false, 'boolean');\nconst cachingAtDepth = (() => {\n const value = getConfigValue('claude.cachingAtDepth', -1, 'number');\n return Number.isInteger(value) && value >= 0 ? value : -1;\n})();\n\n/**\n * Cache for cacheable (writing) OpenRouter model IDs.\n * @type {string[]}\n */\nconst openRouterCacheableModels = [];\n\n/**\n * Checks if an OpenRouter model supports prompt cache writing.\n * Uses a cache to avoid repeated API calls.\n * @param {string} modelId - The OpenRouter model ID\n * @returns {Promise} `true` if the model supports writing cache\n */\nasync function isOpenRouterModelCacheable(modelId) {\n if (openRouterCacheableModels.includes(modelId)) {\n return true;\n }\n\n try {\n const response = await fetch(`${API_OPENROUTER}/models`, {\n method: 'GET',\n headers: { 'Accept': 'application/json' },\n signal: AbortSignal.timeout(5000),\n });\n\n if (!response.ok) {\n console.warn(`OpenRouter models API returned ${response.status}: ${response.statusText}`);\n return false;\n }\n\n /** @type {any} */\n const data = await response.json();\n\n if (!Array.isArray(data?.data)) {\n console.warn('OpenRouter API response format unexpected');\n return false;\n }\n\n const model = data.data.find(m => m.id === modelId);\n const supportsCache = model?.pricing?.input_cache_write != null;\n\n if (supportsCache) {\n openRouterCacheableModels.push(modelId);\n }\n\n return supportsCache;\n } catch (error) {\n console.warn(`Failed to check OpenRouter cache support for ${modelId}:`, error.message);\n return false;\n }\n}\n\n/**\n * Gets OpenRouter transforms based on the request.\n * @param {import('express').Request} request Express request\n * @returns {string[] | undefined} OpenRouter transforms\n */\nfunction getOpenRouterTransforms(request) {\n switch (request.body.middleout) {\n case 'on':\n return ['middle-out'];\n case 'off':\n return [];\n case 'auto':\n return undefined;\n }\n}\n\n/**\n * Gets OpenRouter plugins based on the request.\n * @param {import('express').Request} request\n * @returns {any[]} OpenRouter plugins\n */\nfunction getOpenRouterPlugins(request) {\n const plugins = [];\n\n if (request.body.enable_web_search) {\n plugins.push({ 'id': 'web' });\n }\n\n return plugins;\n}\n\n/**\n * Hacky way to use JSON schema only if json_object format is supported.\n * @param {object} bodyParams Additional body parameters\n * @param {object[]} messages Array of messages\n * @param {object} jsonSchema JSON schema object\n */\nfunction setJsonObjectFormat(bodyParams, messages, jsonSchema) {\n bodyParams['response_format'] = {\n type: 'json_object',\n };\n const message = {\n role: 'user',\n content: `JSON schema for the response:\\n${JSON.stringify(jsonSchema.value, null, 4)}`,\n };\n messages.push(message);\n}\n\n/**\n * Sends a request to Claude API.\n * @param {express.Request} request Express request\n * @param {express.Response} response Express response\n */\nasync function sendClaudeRequest(request, response) {\n const apiUrl = new URL(request.body.reverse_proxy || API_CLAUDE).toString();\n const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.CLAUDE);\n const divider = '-'.repeat(process.stdout.columns);\n\n if (!apiKey) {\n console.warn(color.red(`Claude API key is missing.\\n${divider}`));\n return response.status(400).send({ error: true });\n }\n\n try {\n const controller = new AbortController();\n request.socket.removeAllListeners('close');\n request.socket.on('close', function () {\n controller.abort();\n });\n const additionalHeaders = {};\n const betaHeaders = ['output-128k-2025-02-19'];\n const useTools = Array.isArray(request.body.tools) && request.body.tools.length > 0;\n const useSystemPrompt = Boolean(request.body.use_sysprompt);\n const convertedPrompt = convertClaudeMessages(request.body.messages, request.body.assistant_prefill, useSystemPrompt, useTools, getPromptNames(request));\n const useThinking = /^claude-(3-7|opus-4|sonnet-4|haiku-4-5|opus-4-5)/.test(request.body.model);\n const useWebSearch = /^claude-(3-5|3-7|opus-4|sonnet-4|haiku-4-5|opus-4-5)/.test(request.body.model) && Boolean(request.body.enable_web_search);\n const isLimitedSampling = /^claude-(opus-4-1|sonnet-4-5|haiku-4-5|opus-4-5)/.test(request.body.model);\n const useVerbosity = /^claude-(opus-4-5)/.test(request.body.model);\n let fixThinkingPrefill = false;\n // Add custom stop sequences\n const stopSequences = [];\n if (Array.isArray(request.body.stop)) {\n stopSequences.push(...request.body.stop);\n }\n\n const requestBody = {\n /** @type {any} */ system: [],\n messages: convertedPrompt.messages,\n model: request.body.model,\n max_tokens: request.body.max_tokens,\n stop_sequences: stopSequences,\n temperature: request.body.temperature,\n top_p: request.body.top_p,\n top_k: request.body.top_k,\n stream: request.body.stream,\n };\n if (useSystemPrompt) {\n if (enableSystemPromptCache && Array.isArray(convertedPrompt.systemPrompt) && convertedPrompt.systemPrompt.length) {\n convertedPrompt.systemPrompt[convertedPrompt.systemPrompt.length - 1]['cache_control'] = { type: 'ephemeral', ttl: cacheTTL };\n }\n\n requestBody.system = convertedPrompt.systemPrompt;\n } else {\n delete requestBody.system;\n }\n if (useTools) {\n betaHeaders.push('tools-2024-05-16');\n requestBody.tool_choice = { type: request.body.tool_choice };\n requestBody.tools = request.body.tools\n .filter(tool => tool.type === 'function')\n .map(tool => tool.function)\n .map(fn => ({ name: fn.name, description: fn.description, input_schema: flattenSchema(fn.parameters, request.body.chat_completion_source) }));\n\n if (enableSystemPromptCache && requestBody.tools.length) {\n requestBody.tools[requestBody.tools.length - 1]['cache_control'] = { type: 'ephemeral', ttl: cacheTTL };\n }\n }\n\n // Structured output is a forced tool\n if (request.body.json_schema) {\n const jsonTool = {\n name: request.body.json_schema.name,\n description: request.body.json_schema.description || 'Well-formed JSON object',\n input_schema: request.body.json_schema.value,\n };\n requestBody.tools = [...(requestBody.tools || []), jsonTool];\n requestBody.tool_choice = { type: 'tool', name: request.body.json_schema.name };\n }\n\n if (useWebSearch) {\n const webSearchTool = [{\n 'type': 'web_search_20250305',\n 'name': 'web_search',\n }];\n requestBody.tools = [...webSearchTool, ...(requestBody.tools || [])];\n }\n\n if (cachingAtDepth !== -1) {\n cachingAtDepthForClaude(convertedPrompt.messages, cachingAtDepth, cacheTTL);\n }\n\n if (enableSystemPromptCache || cachingAtDepth !== -1) {\n betaHeaders.push('prompt-caching-2024-07-31');\n betaHeaders.push('extended-cache-ttl-2025-04-11');\n }\n\n if (isLimitedSampling) {\n if (requestBody.top_p < 1) {\n delete requestBody.temperature;\n } else {\n delete requestBody.top_p;\n }\n }\n\n const reasoningEffort = request.body.reasoning_effort;\n const budgetTokens = calculateClaudeBudgetTokens(requestBody.max_tokens, reasoningEffort, requestBody.stream);\n\n if (useThinking && Number.isInteger(budgetTokens)) {\n // No prefill when thinking\n fixThinkingPrefill = true;\n const minThinkTokens = 1024;\n if (requestBody.max_tokens <= minThinkTokens) {\n const newValue = requestBody.max_tokens + minThinkTokens;\n console.warn(color.yellow(`Claude thinking requires a minimum of ${minThinkTokens} response tokens.`));\n console.info(color.blue(`Increasing response length to ${newValue}.`));\n requestBody.max_tokens = newValue;\n }\n requestBody.thinking = {\n type: 'enabled',\n budget_tokens: budgetTokens,\n };\n\n // NO I CAN'T SILENTLY IGNORE THE TEMPERATURE.\n delete requestBody.temperature;\n delete requestBody.top_p;\n delete requestBody.top_k;\n }\n\n if (fixThinkingPrefill && convertedPrompt.messages.length && convertedPrompt.messages[convertedPrompt.messages.length - 1].role === 'assistant') {\n convertedPrompt.messages[convertedPrompt.messages.length - 1].role = 'user';\n }\n\n // Verbosity = 'effort' (same values as OpenAI)\n if (useVerbosity && request.body.verbosity) {\n betaHeaders.push('effort-2025-11-24');\n requestBody.output_config ??= {};\n requestBody.output_config.effort = request.body.verbosity;\n }\n\n if (betaHeaders.length) {\n additionalHeaders['anthropic-beta'] = betaHeaders.join(',');\n }\n\n console.debug('Claude request:', requestBody);\n\n const generateResponse = await fetch(apiUrl + '/messages', {\n method: 'POST',\n signal: controller.signal,\n body: JSON.stringify(requestBody),\n headers: {\n 'Content-Type': 'application/json',\n 'anthropic-version': '2023-06-01',\n 'x-api-key': apiKey,\n ...additionalHeaders,\n },\n });\n\n if (request.body.stream) {\n // Pipe remote SSE stream to Express response\n forwardFetchResponse(generateResponse, response);\n } else {\n if (!generateResponse.ok) {\n const generateResponseText = await generateResponse.text();\n console.warn(color.red(`Claude API returned error: ${generateResponse.status} ${generateResponse.statusText}\\n${generateResponseText}\\n${divider}`));\n return response.status(500).send({ error: true });\n }\n\n /** @type {any} */\n const generateResponseJson = await generateResponse.json();\n const responseText = generateResponseJson?.content?.[0]?.text || '';\n console.debug('Claude response:', generateResponseJson);\n\n // Wrap it back to OAI format + save the original content\n const reply = { choices: [{ 'message': { 'content': responseText } }], content: generateResponseJson.content };\n return response.send(reply);\n }\n } catch (error) {\n console.error(color.red(`Error communicating with Claude: ${error}\\n${divider}`));\n if (!response.headersSent) {\n return response.status(500).send({ error: true });\n }\n }\n}\n\n/**\n * Sends a request to Google AI API.\n * @param {express.Request} request Express request\n * @param {express.Response} response Express response\n */\nasync function sendMakerSuiteRequest(request, response) {\n const useVertexAi = request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.VERTEXAI;\n const apiName = useVertexAi ? 'Google Vertex AI' : 'Google AI Studio';\n let apiUrl;\n let apiKey;\n\n let authHeader;\n let authType;\n\n if (useVertexAi) {\n apiUrl = new URL(request.body.reverse_proxy || API_VERTEX_AI);\n\n try {\n const auth = await getVertexAIAuth(request);\n authHeader = auth.authHeader;\n authType = auth.authType;\n console.debug(`Using Vertex AI authentication type: ${authType}`);\n } catch (error) {\n console.warn(`${apiName} authentication failed: ${error.message}`);\n return response.status(400).send({ error: true, message: error.message });\n }\n } else {\n apiUrl = new URL(request.body.reverse_proxy || API_MAKERSUITE);\n apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MAKERSUITE);\n\n if (!request.body.reverse_proxy && !apiKey) {\n console.warn(`${apiName} API key is missing.`);\n return response.status(400).send({ error: true });\n }\n\n authHeader = `Bearer ${apiKey}`;\n authType = 'api_key';\n }\n\n const model = String(request.body.model);\n const stream = Boolean(request.body.stream);\n const enableWebSearch = Boolean(request.body.enable_web_search);\n const requestImages = Boolean(request.body.request_images);\n const reasoningEffort = String(request.body.reasoning_effort);\n const includeReasoning = Boolean(request.body.include_reasoning);\n const aspectRatio = String(request.body.request_image_aspect_ratio);\n const imageSize = String(request.body.request_image_resolution);\n const isGemma = model.includes('gemma');\n const isLearnLM = model.includes('learnlm');\n\n const responseMimeType = request.body.responseMimeType ?? (request.body.json_schema ? 'application/json' : undefined);\n const responseSchema = request.body.responseSchema ?? (request.body.json_schema ? request.body.json_schema.value : undefined);\n\n const generationConfig = {\n stopSequences: request.body.stop,\n candidateCount: 1,\n maxOutputTokens: request.body.max_tokens,\n temperature: request.body.temperature,\n topP: request.body.top_p,\n topK: request.body.top_k || undefined,\n responseMimeType: responseMimeType,\n responseSchema: responseSchema,\n seed: request.body.seed,\n };\n\n function getGeminiBody() {\n // #region UGLY MODEL LISTS AREA\n const imageGenerationModels = [\n 'gemini-2.0-flash-exp',\n 'gemini-2.0-flash-exp-image-generation',\n 'gemini-2.0-flash-preview-image-generation',\n 'gemini-2.5-flash-image-preview',\n 'gemini-2.5-flash-image',\n 'gemini-3-pro-image-preview',\n ];\n\n const isThinkingConfigModel = m => (/^gemini-2.5-(flash|pro)/.test(m) && !/-image(-preview)?$/.test(m)) || (/^gemini-3-(flash|pro)/.test(m));\n const isImageSizeModel = m => /^gemini-3/.test(m);\n\n const noSearchModels = [\n 'gemini-2.0-flash-lite',\n 'gemini-2.0-flash-lite-001',\n 'gemini-2.0-flash-lite-preview-02-05',\n 'gemini-robotics-er-1.5-preview',\n ];\n // #endregion\n\n if (!Array.isArray(generationConfig.stopSequences) || !generationConfig.stopSequences.length) {\n delete generationConfig.stopSequences;\n }\n\n const enableImageModality = requestImages && imageGenerationModels.includes(model);\n const enableImageConfig = enableImageModality && (aspectRatio || imageSize);\n if (enableImageModality) {\n generationConfig.responseModalities = ['text', 'image'];\n if (enableImageConfig) {\n generationConfig.imageConfig = {};\n if (imageSize && isImageSizeModel(model)) {\n generationConfig.imageConfig.imageSize = imageSize;\n }\n if (aspectRatio) {\n generationConfig.imageConfig.aspectRatio = aspectRatio;\n }\n }\n }\n\n const useSystemPrompt = !enableImageModality && !isGemma && request.body.use_sysprompt;\n\n const tools = [];\n const prompt = convertGooglePrompt(request.body.messages, model, useSystemPrompt, getPromptNames(request));\n const safetySettings = [...GEMINI_SAFETY, ...(useVertexAi ? VERTEX_SAFETY : [])];\n\n if (Array.isArray(request.body.tools) && request.body.tools.length > 0 && !enableImageModality && !isGemma) {\n const functionDeclarations = [];\n const customTools = [];\n for (const tool of request.body.tools) {\n if (tool.type === 'function') {\n if (tool.function.parameters?.$schema) {\n delete tool.function.parameters.$schema;\n }\n if (tool.function.parameters?.properties && Object.keys(tool.function.parameters.properties).length === 0) {\n delete tool.function.parameters;\n }\n functionDeclarations.push(tool.function);\n } else if (tool[tool.type]) {\n customTools.push({ [tool.type]: tool[tool.type] });\n }\n }\n if (functionDeclarations.length > 0) {\n tools.push({ function_declarations: functionDeclarations });\n }\n // Custom tools are only supported when no function calling is present\n if (functionDeclarations.length === 0 && customTools.length > 0) {\n tools.push(...customTools);\n }\n }\n\n if (enableWebSearch && !enableImageModality && !isGemma && !isLearnLM && !noSearchModels.includes(model)) {\n // Tool use with function calling is unsupported\n if (!tools.some(t => t.function_declarations)) {\n tools.push({ google_search: {} });\n }\n }\n\n if (isThinkingConfigModel(model)) {\n const thinkingConfig = { includeThoughts: includeReasoning };\n\n const thinkingBudget = calculateGoogleBudgetTokens(generationConfig.maxOutputTokens, reasoningEffort, model);\n if (typeof thinkingBudget === 'number' && Number.isInteger(thinkingBudget)) {\n thinkingConfig.thinkingBudget = thinkingBudget;\n }\n\n if (typeof thinkingBudget === 'string' && thinkingBudget.length > 0) {\n thinkingConfig.thinkingLevel = thinkingBudget;\n }\n\n // Vertex doesn't allow mixing disabled thinking with includeThoughts\n if (useVertexAi && thinkingBudget === 0 && thinkingConfig.includeThoughts) {\n console.info('Thinking budget is 0, but includeThoughts is true. Thoughts will not be included in the response.');\n thinkingConfig.includeThoughts = false;\n }\n\n generationConfig.thinkingConfig = thinkingConfig;\n }\n\n let body = {\n contents: prompt.contents,\n safetySettings: safetySettings,\n generationConfig: generationConfig,\n };\n\n if (useSystemPrompt && Array.isArray(prompt.system_instruction.parts) && prompt.system_instruction.parts.length) {\n body.systemInstruction = prompt.system_instruction;\n }\n\n if (tools.length) {\n body.tools = tools;\n\n const toolChoice = request.body.tool_choice;\n let functionCallingConfig;\n\n // Translate OpenAI's `tool_choice` to Gemini's `functionCallingConfig`\n if (typeof toolChoice === 'string') {\n switch (toolChoice) {\n case 'none':\n functionCallingConfig = { mode: 'NONE' };\n break;\n case 'required':\n functionCallingConfig = { mode: 'ANY' };\n break;\n case 'auto':\n functionCallingConfig = { mode: 'AUTO' };\n break;\n }\n } else if (typeof toolChoice === 'object' && toolChoice?.function?.name) {\n // Force a specific function call\n functionCallingConfig = {\n mode: 'ANY',\n allowedFunctionNames: [toolChoice.function.name],\n };\n }\n\n if (functionCallingConfig) {\n body.toolConfig = { functionCallingConfig };\n }\n }\n\n return body;\n }\n\n const body = getGeminiBody();\n console.debug(`${apiName} request:`, body);\n\n try {\n const controller = new AbortController();\n request.socket.removeAllListeners('close');\n request.socket.on('close', function () {\n controller.abort();\n });\n\n const apiVersion = getConfigValue('gemini.apiVersion', 'v1beta');\n const responseType = (stream ? 'streamGenerateContent' : 'generateContent');\n\n let url;\n let headers = {\n 'Content-Type': 'application/json',\n };\n\n if (useVertexAi) {\n if (authType === 'express') {\n // For Express mode (API key authentication), use the key parameter\n const keyParam = authHeader.replace('Bearer ', '');\n const region = request.body.vertexai_region || 'us-central1';\n const projectId = request.body.vertexai_express_project_id;\n const baseUrl = region === 'global'\n ? 'https://aiplatform.googleapis.com'\n : `https://${region}-aiplatform.googleapis.com`;\n url = projectId\n ? `https://aiplatform.googleapis.com/v1/projects/${projectId}/locations/${region}/publishers/google/models/${model}:${responseType}?key=${keyParam}${stream ? '&alt=sse' : ''}`\n : `${baseUrl}/v1/publishers/google/models/${model}:${responseType}?key=${keyParam}${stream ? '&alt=sse' : ''}`;\n } else if (authType === 'full') {\n // For Full mode (service account authentication), use project-specific URL\n // Get project ID from Service Account JSON\n const serviceAccountJson = readSecret(request.user.directories, SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT);\n if (!serviceAccountJson) {\n console.warn('Vertex AI Service Account JSON is missing.');\n return response.status(400).send({ error: true });\n }\n\n let projectId;\n try {\n const serviceAccount = JSON.parse(serviceAccountJson);\n projectId = getProjectIdFromServiceAccount(serviceAccount);\n } catch (error) {\n console.error('Failed to extract project ID from Service Account JSON:', error);\n return response.status(400).send({ error: true });\n }\n const region = request.body.vertexai_region || 'us-central1';\n // Handle global region differently - no region prefix in hostname\n if (region === 'global') {\n url = `https://aiplatform.googleapis.com/v1/projects/${projectId}/locations/${region}/publishers/google/models/${model}:${responseType}${stream ? '?alt=sse' : ''}`;\n } else {\n url = `https://${region}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${region}/publishers/google/models/${model}:${responseType}${stream ? '?alt=sse' : ''}`;\n }\n headers['Authorization'] = authHeader;\n } else {\n // For proxy mode, use the original URL with Authorization header\n url = `${apiUrl.toString().replace(/\\/$/, '')}/v1/publishers/google/models/${model}:${responseType}${stream ? '?alt=sse' : ''}`;\n headers['Authorization'] = authHeader;\n }\n } else {\n url = `${apiUrl.toString().replace(/\\/$/, '')}/${apiVersion}/models/${model}:${responseType}?key=${apiKey}${stream ? '&alt=sse' : ''}`;\n }\n\n const generateResponse = await fetch(url, {\n body: JSON.stringify(body),\n method: 'POST',\n headers: headers,\n signal: controller.signal,\n });\n\n if (stream) {\n try {\n // Pipe remote SSE stream to Express response\n forwardFetchResponse(generateResponse, response);\n } catch (error) {\n console.error('Error forwarding streaming response:', error);\n if (!response.headersSent) {\n return response.status(500).send({ error: true });\n }\n }\n } else {\n if (!generateResponse.ok) {\n const errorText = await generateResponse.text();\n console.warn(`${apiName} API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);\n const errorJson = tryParse(errorText) ?? { error: true };\n return response.status(500).send(errorJson);\n }\n\n /** @type {any} */\n const generateResponseJson = await generateResponse.json();\n\n const candidates = generateResponseJson?.candidates;\n if (!candidates || candidates.length === 0) {\n let message = `${apiName} API returned no candidate`;\n console.warn(message, generateResponseJson);\n if (generateResponseJson?.promptFeedback?.blockReason) {\n message += `\\nPrompt was blocked due to : ${generateResponseJson.promptFeedback.blockReason}`;\n }\n return response.send({ error: { message } });\n }\n\n const responseContent = candidates[0].content ?? candidates[0].output;\n const functionCall = (candidates?.[0]?.content?.parts ?? []).some(part => part.functionCall);\n const inlineData = (candidates?.[0]?.content?.parts ?? []).some(part => part.inlineData);\n console.debug(`${apiName} response:`, util.inspect(generateResponseJson, { depth: 5, colors: true }));\n\n const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.filter(part => !part.thought)?.map(part => part.text)?.join('\\n\\n');\n if (!responseText && !functionCall && !inlineData) {\n let message = `${apiName} Candidate text empty`;\n console.warn(message, generateResponseJson);\n return response.send({ error: { message } });\n }\n\n // Wrap it back to OAI format (responseContent includes thought signatures in parts array)\n const reply = { choices: [{ 'message': { 'content': responseText } }], responseContent };\n return response.send(reply);\n }\n } catch (error) {\n console.error(`Error communicating with ${apiName} API:`, error);\n if (!response.headersSent) {\n return response.status(500).send({ error: true });\n }\n }\n}\n\n/**\n * Sends a request to AI21 API.\n * @param {express.Request} request Express request\n * @param {express.Response} response Express response\n */\nasync function sendAI21Request(request, response) {\n if (!request.body) return response.sendStatus(400);\n\n const apiKey = readSecret(request.user.directories, SECRET_KEYS.AI21);\n if (!apiKey) {\n console.warn('AI21 API key is missing.');\n return response.status(400).send({ error: true });\n }\n\n const bodyParams = {};\n const controller = new AbortController();\n request.socket.removeAllListeners('close');\n request.socket.on('close', function () {\n controller.abort();\n });\n // Hack to support JSON schema\n if (request.body.json_schema) {\n bodyParams.response_format = {\n type: 'json_object',\n };\n const message = {\n role: 'user',\n content: `JSON schema for the response:\\n${JSON.stringify(request.body.json_schema.value, null, 4)}`,\n };\n request.body.messages.push(message);\n }\n const convertedPrompt = convertAI21Messages(request.body.messages, getPromptNames(request));\n const body = {\n messages: convertedPrompt,\n model: request.body.model,\n max_tokens: request.body.max_tokens,\n temperature: request.body.temperature,\n top_p: request.body.top_p,\n stop: request.body.stop,\n stream: request.body.stream,\n tools: request.body.tools,\n ...bodyParams,\n };\n const options = {\n method: 'POST',\n headers: {\n accept: 'application/json',\n 'content-type': 'application/json',\n Authorization: `Bearer ${apiKey}`,\n },\n body: JSON.stringify(body),\n signal: controller.signal,\n };\n\n console.debug('AI21 request:', body);\n\n try {\n const generateResponse = await fetch(API_AI21 + '/chat/completions', options);\n if (request.body.stream) {\n forwardFetchResponse(generateResponse, response);\n } else {\n if (!generateResponse.ok) {\n const errorText = await generateResponse.text();\n console.warn(`AI21 API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);\n const errorJson = tryParse(errorText) ?? { error: true };\n return response.status(500).send(errorJson);\n }\n const generateResponseJson = await generateResponse.json();\n console.debug('AI21 response:', generateResponseJson);\n return response.send(generateResponseJson);\n }\n } catch (error) {\n console.error('Error communicating with AI21 API: ', error);\n if (!response.headersSent) {\n response.send({ error: true });\n } else {\n response.end();\n }\n }\n}\n\n/**\n * Sends a request to MistralAI API.\n * @param {express.Request} request Express request\n * @param {express.Response} response Express response\n */\nasync function sendMistralAIRequest(request, response) {\n const apiUrl = new URL(request.body.reverse_proxy || API_MISTRAL).toString();\n const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MISTRALAI);\n\n if (!apiKey) {\n console.warn('MistralAI API key is missing.');\n return response.status(400).send({ error: true });\n }\n\n try {\n const messages = convertMistralMessages(request.body.messages, getPromptNames(request));\n const controller = new AbortController();\n request.socket.removeAllListeners('close');\n request.socket.on('close', function () {\n controller.abort();\n });\n\n const requestBody = {\n 'model': request.body.model,\n 'messages': messages,\n 'temperature': request.body.temperature,\n 'top_p': request.body.top_p,\n 'frequency_penalty': request.body.frequency_penalty,\n 'presence_penalty': request.body.presence_penalty,\n 'max_tokens': request.body.max_tokens,\n 'stream': request.body.stream,\n 'safe_prompt': request.body.safe_prompt,\n 'random_seed': request.body.seed === -1 ? undefined : request.body.seed,\n 'stop': Array.isArray(request.body.stop) && request.body.stop.length > 0 ? request.body.stop : undefined,\n };\n\n if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {\n requestBody['tools'] = request.body.tools;\n requestBody['tool_choice'] = request.body.tool_choice;\n }\n\n if (request.body.json_schema) {\n requestBody['response_format'] = {\n type: 'json_schema',\n json_schema: {\n name: request.body.json_schema.name,\n description: request.body.json_schema.description,\n schema: request.body.json_schema.value,\n strict: request.body.json_schema.strict ?? true,\n },\n };\n }\n\n const config = {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer ' + apiKey,\n },\n body: JSON.stringify(requestBody),\n signal: controller.signal,\n timeout: 0,\n };\n\n console.debug('MisralAI request:', requestBody);\n\n const generateResponse = await fetch(apiUrl + '/chat/completions', config);\n if (request.body.stream) {\n forwardFetchResponse(generateResponse, response);\n } else {\n if (!generateResponse.ok) {\n const errorText = await generateResponse.text();\n console.warn(`MistralAI API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);\n const errorJson = tryParse(errorText) ?? { error: true };\n return response.status(500).send(errorJson);\n }\n const generateResponseJson = await generateResponse.json();\n console.debug('MistralAI response:', generateResponseJson);\n return response.send(generateResponseJson);\n }\n } catch (error) {\n console.error('Error communicating with MistralAI API: ', error);\n if (!response.headersSent) {\n response.send({ error: true });\n } else {\n response.end();\n }\n }\n}\n\n/**\n * Sends a request to Cohere API.\n * @param {express.Request} request Express request\n * @param {express.Response} response Express response\n */\nasync function sendCohereRequest(request, response) {\n const apiKey = readSecret(request.user.directories, SECRET_KEYS.COHERE);\n const controller = new AbortController();\n request.socket.removeAllListeners('close');\n request.socket.on('close', function () {\n controller.abort();\n });\n\n if (!apiKey) {\n console.warn('Cohere API key is missing.');\n return response.status(400).send({ error: true });\n }\n\n try {\n const convertedHistory = convertCohereMessages(request.body.messages, getPromptNames(request));\n const tools = [];\n\n if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {\n tools.push(...request.body.tools);\n tools.forEach(tool => {\n if (tool?.function?.parameters?.$schema) {\n delete tool.function.parameters.$schema;\n }\n });\n }\n\n // https://docs.cohere.com/reference/chat\n const requestBody = {\n stream: Boolean(request.body.stream),\n model: request.body.model,\n messages: convertedHistory.chatHistory,\n temperature: request.body.temperature,\n max_tokens: request.body.max_tokens,\n k: request.body.top_k,\n p: request.body.top_p,\n seed: request.body.seed,\n stop_sequences: request.body.stop,\n frequency_penalty: request.body.frequency_penalty,\n presence_penalty: request.body.presence_penalty,\n documents: [],\n tools: tools,\n };\n\n const canDoSafetyMode = String(request.body.model).endsWith('08-2024');\n if (canDoSafetyMode) {\n requestBody.safety_mode = 'OFF';\n }\n\n if (request.body.json_schema) {\n requestBody.response_format = {\n type: 'json_schema',\n schema: request.body.json_schema.value,\n };\n }\n\n console.debug('Cohere request:', requestBody);\n\n const config = {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer ' + apiKey,\n },\n body: JSON.stringify(requestBody),\n signal: controller.signal,\n timeout: 0,\n };\n\n const apiUrl = API_COHERE_V2 + '/chat';\n\n if (request.body.stream) {\n const stream = await fetch(apiUrl, config);\n forwardFetchResponse(stream, response);\n } else {\n const generateResponse = await fetch(apiUrl, config);\n if (!generateResponse.ok) {\n const errorText = await generateResponse.text();\n console.warn(`Cohere API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);\n const errorJson = tryParse(errorText) ?? { error: true };\n return response.status(500).send(errorJson);\n }\n const generateResponseJson = await generateResponse.json();\n console.debug('Cohere response:', generateResponseJson);\n return response.send(generateResponseJson);\n }\n } catch (error) {\n console.error('Error communicating with Cohere API: ', error);\n if (!response.headersSent) {\n response.send({ error: true });\n } else {\n response.end();\n }\n }\n}\n\n/**\n * Sends a request to DeepSeek API.\n * @param {express.Request} request Express request\n * @param {express.Response} response Express response\n */\nasync function sendDeepSeekRequest(request, response) {\n const apiUrl = new URL(request.body.reverse_proxy || API_DEEPSEEK).toString();\n const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK);\n\n if (!apiKey && !request.body.reverse_proxy) {\n console.warn('DeepSeek API key is missing.');\n return response.status(400).send({ error: true });\n }\n\n const controller = new AbortController();\n request.socket.removeAllListeners('close');\n request.socket.on('close', function () {\n controller.abort();\n });\n\n try {\n let bodyParams = {};\n\n if (request.body.logprobs > 0) {\n bodyParams['top_logprobs'] = request.body.logprobs;\n bodyParams['logprobs'] = true;\n }\n\n if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {\n bodyParams['tools'] = request.body.tools;\n bodyParams['tool_choice'] = request.body.tool_choice;\n\n // DeepSeek doesn't permit empty required arrays\n bodyParams.tools.forEach(tool => {\n const required = tool?.function?.parameters?.required;\n if (Array.isArray(required) && required.length === 0) {\n delete tool.function.parameters.required;\n }\n });\n }\n\n // Hack to support JSON schema\n if (request.body.json_schema) {\n bodyParams.response_format = {\n type: 'json_object',\n };\n const message = {\n role: 'user',\n content: `JSON schema for the response:\\n${JSON.stringify(request.body.json_schema.value, null, 4)}`,\n };\n request.body.messages.push(message);\n }\n\n const processedMessages = addAssistantPrefix(postProcessPrompt(request.body.messages, PROMPT_PROCESSING_TYPE.SEMI_TOOLS, getPromptNames(request)), bodyParams.tools, 'prefix');\n\n if (/-reasoner/.test(request.body.model)) {\n addReasoningContentToToolCalls(processedMessages);\n }\n\n const requestBody = {\n 'messages': processedMessages,\n 'model': request.body.model,\n 'temperature': request.body.temperature,\n 'max_tokens': request.body.max_tokens,\n 'stream': request.body.stream,\n 'presence_penalty': request.body.presence_penalty,\n 'frequency_penalty': request.body.frequency_penalty,\n 'top_p': request.body.top_p,\n 'stop': request.body.stop,\n 'seed': request.body.seed,\n ...bodyParams,\n };\n\n const config = {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer ' + apiKey,\n },\n body: JSON.stringify(requestBody),\n signal: controller.signal,\n };\n\n console.debug('DeepSeek request:', requestBody);\n\n const generateResponse = await fetch(apiUrl + '/chat/completions', config);\n\n if (request.body.stream) {\n forwardFetchResponse(generateResponse, response);\n } else {\n if (!generateResponse.ok) {\n const errorText = await generateResponse.text();\n console.warn(`DeepSeek API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);\n const errorJson = tryParse(errorText) ?? { error: true };\n return response.status(500).send(errorJson);\n }\n const generateResponseJson = await generateResponse.json();\n console.debug('DeepSeek response:', generateResponseJson);\n return response.send(generateResponseJson);\n }\n } catch (error) {\n console.error('Error communicating with DeepSeek API: ', error);\n if (!response.headersSent) {\n response.send({ error: true });\n } else {\n response.end();\n }\n }\n}\n\n/**\n * Sends a request to XAI API.\n * @param {express.Request} request Express request\n * @param {express.Response} response Express response\n */\nasync function sendXaiRequest(request, response) {\n const apiUrl = new URL(request.body.reverse_proxy || API_XAI).toString();\n const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.XAI);\n\n if (!apiKey && !request.body.reverse_proxy) {\n console.warn('xAI API key is missing.');\n return response.status(400).send({ error: true });\n }\n\n const controller = new AbortController();\n request.socket.removeAllListeners('close');\n request.socket.on('close', function () {\n controller.abort();\n });\n\n try {\n let bodyParams = {};\n\n if (request.body.logprobs > 0) {\n bodyParams['top_logprobs'] = request.body.logprobs;\n bodyParams['logprobs'] = true;\n }\n\n if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {\n bodyParams['tools'] = request.body.tools;\n bodyParams['tool_choice'] = request.body.tool_choice;\n }\n\n if (Array.isArray(request.body.stop) && request.body.stop.length > 0) {\n bodyParams['stop'] = request.body.stop;\n }\n\n if (request.body.reasoning_effort) {\n bodyParams['reasoning_effort'] = request.body.reasoning_effort === 'high' ? 'high' : 'low';\n }\n\n if (request.body.enable_web_search) {\n bodyParams['search_parameters'] = {\n mode: 'on',\n sources: [\n { type: 'web', safe_search: false },\n { type: 'news', safe_search: false },\n { type: 'x' },\n ],\n };\n }\n\n if (request.body.json_schema) {\n bodyParams['response_format'] = {\n type: 'json_schema',\n json_schema: {\n name: request.body.json_schema.name,\n strict: request.body.json_schema.strict ?? true,\n schema: request.body.json_schema.value,\n },\n };\n }\n\n const processedMessages = request.body.messages = convertXAIMessages(request.body.messages, getPromptNames(request));\n\n const requestBody = {\n 'messages': processedMessages,\n 'model': request.body.model,\n 'temperature': request.body.temperature,\n 'max_tokens': request.body.max_tokens,\n 'max_completion_tokens': request.body.max_completion_tokens,\n 'stream': request.body.stream,\n 'presence_penalty': request.body.presence_penalty,\n 'frequency_penalty': request.body.frequency_penalty,\n 'top_p': request.body.top_p,\n 'seed': request.body.seed,\n 'n': request.body.n,\n ...bodyParams,\n };\n\n const config = {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer ' + apiKey,\n },\n body: JSON.stringify(requestBody),\n signal: controller.signal,\n };\n\n console.debug('xAI request:', requestBody);\n\n const generateResponse = await fetch(apiUrl + '/chat/completions', config);\n\n if (request.body.stream) {\n forwardFetchResponse(generateResponse, response);\n } else {\n if (!generateResponse.ok) {\n const errorText = await generateResponse.text();\n console.warn(`xAI API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);\n const errorJson = tryParse(errorText) ?? { error: true };\n return response.status(500).send(errorJson);\n }\n const generateResponseJson = await generateResponse.json();\n console.debug('xAI response:', generateResponseJson);\n return response.send(generateResponseJson);\n }\n } catch (error) {\n console.error('Error communicating with xAI API: ', error);\n if (!response.headersSent) {\n response.send({ error: true });\n } else {\n response.end();\n }\n }\n}\n\n/**\n * Sends a request to AI/ML API.\n * @param {express.Request} request Express request\n * @param {express.Response} response Express response\n */\nasync function sendAimlapiRequest(request, response) {\n const apiUrl = API_AIMLAPI;\n const apiKey = readSecret(request.user.directories, SECRET_KEYS.AIMLAPI);\n\n if (!apiKey) {\n console.warn('AI/ML API key is missing.');\n return response.status(400).send({ error: true });\n }\n\n const controller = new AbortController();\n request.socket.removeAllListeners('close');\n request.socket.on('close', function () {\n controller.abort();\n });\n\n try {\n let bodyParams = {};\n\n if (request.body.logprobs > 0) {\n bodyParams['top_logprobs'] = request.body.logprobs;\n bodyParams['logprobs'] = true;\n }\n\n if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {\n bodyParams['tools'] = request.body.tools;\n bodyParams['tool_choice'] = request.body.tool_choice;\n }\n\n if (Array.isArray(request.body.stop) && request.body.stop.length > 0) {\n bodyParams['stop'] = request.body.stop;\n }\n\n if (request.body.reasoning_effort) {\n bodyParams['reasoning_effort'] = request.body.reasoning_effort;\n }\n\n if (request.body.json_schema) {\n bodyParams['response_format'] = {\n type: 'json_schema',\n json_schema: {\n name: request.body.json_schema.name,\n description: request.body.json_schema.description,\n schema: request.body.json_schema.value,\n strict: request.body.json_schema.strict ?? true,\n },\n };\n }\n\n const requestBody = {\n 'messages': request.body.messages,\n 'model': request.body.model,\n 'temperature': request.body.temperature,\n 'max_tokens': request.body.max_tokens,\n 'stream': request.body.stream,\n 'presence_penalty': request.body.presence_penalty,\n 'frequency_penalty': request.body.frequency_penalty,\n 'top_p': request.body.top_p,\n 'seed': request.body.seed,\n 'n': request.body.n,\n ...bodyParams,\n };\n\n const config = {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer ' + apiKey,\n ...AIMLAPI_HEADERS,\n },\n body: JSON.stringify(requestBody),\n signal: controller.signal,\n };\n\n console.debug('AI/ML API request:', requestBody);\n\n const generateResponse = await fetch(apiUrl + '/chat/completions', config);\n\n if (request.body.stream) {\n forwardFetchResponse(generateResponse, response);\n } else {\n if (!generateResponse.ok) {\n const errorText = await generateResponse.text();\n console.warn(`AI/ML API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);\n const errorJson = tryParse(errorText) ?? { error: true };\n return response.status(500).send(errorJson);\n }\n const generateResponseJson = await generateResponse.json();\n console.debug('AI/ML API response:', generateResponseJson);\n return response.send(generateResponseJson);\n }\n } catch (error) {\n console.error('Error communicating with AI/ML API: ', error);\n if (!response.headersSent) {\n response.send({ error: true });\n } else {\n response.end();\n }\n }\n}\n\n/**\n * Sends a request to Electron Hub.\n * @param {express.Request} request Express request\n * @param {express.Response} response Express response\n */\nasync function sendElectronHubRequest(request, response) {\n const apiUrl = API_ELECTRONHUB;\n const apiKey = readSecret(request.user.directories, SECRET_KEYS.ELECTRONHUB);\n\n if (!apiKey) {\n console.warn('Electron Hub key is missing.');\n return response.status(400).send({ error: true });\n }\n\n const controller = new AbortController();\n request.socket.removeAllListeners('close');\n request.socket.on('close', function () {\n controller.abort();\n });\n\n try {\n let bodyParams = {};\n\n if (request.body.enable_web_search) {\n bodyParams['web_search'] = true;\n }\n\n if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {\n bodyParams['tools'] = request.body.tools;\n bodyParams['tool_choice'] = request.body.tool_choice;\n }\n\n if (request.body.reasoning_effort) {\n bodyParams['reasoning_effort'] = request.body.reasoning_effort;\n }\n\n if (request.body.json_schema) {\n bodyParams['response_format'] = {\n type: 'json_schema',\n json_schema: {\n name: request.body.json_schema.name,\n description: request.body.json_schema.description,\n schema: request.body.json_schema.value,\n strict: request.body.json_schema.strict ?? true,\n },\n };\n }\n\n const isClaude = /^claude-/.test(request.body.model);\n\n if (Array.isArray(request.body.messages) && isClaude) {\n if (enableSystemPromptCache) {\n cachingSystemPromptForOpenRouter(request.body.messages, cacheTTL);\n }\n\n if (cachingAtDepth !== -1) {\n cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth, cacheTTL);\n }\n }\n\n const requestBody = {\n 'messages': request.body.messages,\n 'model': request.body.model,\n 'temperature': request.body.temperature,\n 'max_tokens': request.body.max_tokens,\n 'stream': request.body.stream,\n 'presence_penalty': request.body.presence_penalty,\n 'frequency_penalty': request.body.frequency_penalty,\n 'top_p': request.body.top_p,\n 'top_k': request.body.top_k,\n 'logit_bias': request.body.logit_bias,\n 'seed': request.body.seed,\n ...bodyParams,\n };\n\n const config = {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer ' + apiKey,\n },\n body: JSON.stringify(requestBody),\n signal: controller.signal,\n };\n\n console.debug('Electron Hub request:', requestBody);\n\n const generateResponse = await fetch(apiUrl + '/chat/completions', config);\n\n if (request.body.stream) {\n forwardFetchResponse(generateResponse, response);\n } else {\n if (!generateResponse.ok) {\n const errorText = await generateResponse.text();\n console.warn('Electron Hub returned error: ', errorText);\n const errorJson = tryParse(errorText) ?? { error: true };\n return response.status(500).send(errorJson);\n }\n const generateResponseJson = await generateResponse.json();\n console.debug('Electron Hub response:', generateResponseJson);\n return response.send(generateResponseJson);\n }\n }\n catch (error) {\n console.error('Error communicating with Electron Hub: ', error);\n if (!response.headersSent) {\n response.send({ error: true });\n } else {\n response.end();\n }\n }\n}\n\n/**\n * Sends a request to Chutes.\n * @param {express.Request} request Express request\n * @param {express.Response} response Express response\n */\nasync function sendChutesRequest(request, response) {\n const apiUrl = API_CHUTES;\n const apiKey = readSecret(request.user.directories, SECRET_KEYS.CHUTES);\n\n if (!apiKey) {\n console.warn('Chutes key is missing.');\n return response.status(400).send({ error: true });\n }\n\n const controller = new AbortController();\n request.socket.removeAllListeners('close');\n request.socket.on('close', function () {\n controller.abort();\n });\n\n try {\n let bodyParams = {};\n\n if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {\n bodyParams['tools'] = request.body.tools;\n bodyParams['tool_choice'] = request.body.tool_choice;\n }\n\n if (request.body.logprobs > 0) {\n bodyParams['top_logprobs'] = request.body.logprobs;\n bodyParams['logprobs'] = true;\n }\n\n if (request.body.json_schema) {\n bodyParams['response_format'] = {\n type: 'json_schema',\n json_schema: {\n name: request.body.json_schema.name,\n description: request.body.json_schema.description,\n schema: request.body.json_schema.value,\n strict: request.body.json_schema.strict ?? true,\n },\n };\n }\n\n const requestBody = {\n 'messages': request.body.messages,\n 'model': request.body.model,\n 'temperature': request.body.temperature,\n 'max_tokens': request.body.max_tokens,\n 'stream': request.body.stream,\n 'presence_penalty': request.body.presence_penalty,\n 'frequency_penalty': request.body.frequency_penalty,\n 'repetition_penalty': request.body.repetition_penalty,\n 'min_p': request.body.min_p,\n 'top_p': request.body.top_p,\n 'top_k': request.body.top_k,\n 'seed': request.body.seed,\n 'stop': request.body.stop,\n 'reasoning_effort': request.body.reasoning_effort,\n 'logit_bias': request.body.logit_bias,\n ...bodyParams,\n };\n\n const config = {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer ' + apiKey,\n },\n body: JSON.stringify(requestBody),\n signal: controller.signal,\n };\n\n console.debug('Chutes request:', requestBody);\n\n const generateResponse = await fetch(apiUrl + '/chat/completions', config);\n\n if (request.body.stream) {\n forwardFetchResponse(generateResponse, response);\n } else {\n if (!generateResponse.ok) {\n const errorText = await generateResponse.text();\n console.warn('Chutes returned error: ', errorText);\n const errorJson = tryParse(errorText) ?? { error: true };\n return response.status(500).send(errorJson);\n }\n const generateResponseJson = await generateResponse.json();\n console.debug('Chutes response:', generateResponseJson);\n return response.send(generateResponseJson);\n }\n }\n catch (error) {\n console.error('Error communicating with Chutes: ', error);\n if (!response.headersSent) {\n response.send({ error: true });\n } else {\n response.end();\n }\n }\n}\n\n/**\n * Sends a chat completion request to Azure OpenAI.\n * @param {express.Request} request Express request object (contains request.body with all generate_data)\n * @param {express.Response} response Express response object\n */\nasync function sendAzureOpenAIRequest(request, response) {\n // 1. GATHER & VALIDATE SETTINGS\n const { azure_base_url, azure_deployment_name, azure_api_version } = request.body;\n const apiKey = readSecret(request.user.directories, SECRET_KEYS.AZURE_OPENAI);\n if (!azure_base_url || !azure_deployment_name || !azure_api_version || !apiKey) {\n return response.status(400).send({\n error: {\n message: 'Azure OpenAI configuration is incomplete. Please provide Base URL, Deployment Name, API Version, and API Key in the connection settings.',\n },\n });\n }\n\n // 2. PREPARE THE REQUEST\n const url = new URL(`/openai/deployments/${azure_deployment_name}/chat/completions`, azure_base_url);\n url.searchParams.set('api-version', azure_api_version);\n const endpointUrl = url.toString();\n\n // Create the base payload with all standard parameters\n const apiRequestBody = /** @type {any} */ ({});\n for (const key of AZURE_OPENAI_KEYS) {\n if (Object.hasOwn(request.body, key)) {\n apiRequestBody[key] = request.body[key];\n }\n }\n\n // Handle Structured Output (JSON Mode) by translating the custom `json_schema` object.\n if (request.body.json_schema) {\n apiRequestBody['response_format'] = {\n type: 'json_schema',\n json_schema: {\n name: request.body.json_schema.name,\n strict: request.body.json_schema.strict ?? true,\n schema: request.body.json_schema.value,\n },\n };\n }\n\n // Adjust logprobs for Azure OpenAI, which follows the OpenAI Chat Completions API spec.\n if (typeof apiRequestBody.logprobs === 'number' && apiRequestBody.logprobs > 0) {\n apiRequestBody.top_logprobs = apiRequestBody.logprobs;\n apiRequestBody.logprobs = true;\n }\n\n // Do not send reasoning effort to models which do not support it\n apiRequestBody['reasoning_effort'] = OPENAI_REASONING_EFFORT_MODELS.includes(request.body.model)\n ? OPENAI_REASONING_EFFORT_MAP[request.body.reasoning_effort] ?? request.body.reasoning_effort\n : undefined;\n\n const controller = new AbortController();\n request.socket.removeAllListeners('close');\n request.socket.on('close', () => controller.abort());\n\n const config = {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'api-key': apiKey,\n },\n body: JSON.stringify(apiRequestBody),\n signal: controller.signal,\n };\n\n console.info(`Sending request to Azure OpenAI: ${endpointUrl}`);\n console.debug('Azure OpenAI Request Body:', apiRequestBody);\n try {\n const fetchResponse = await fetch(endpointUrl, config);\n\n if (request.body.stream) {\n return forwardFetchResponse(fetchResponse, response);\n }\n\n if (fetchResponse.ok) {\n /** @type {any} */\n const json = await fetchResponse.json();\n console.debug('Azure OpenAI response:', json);\n return response.send(json);\n }\n\n const text = await fetchResponse.text();\n const data = tryParse(text) || { error: { message: fetchResponse.statusText || 'Unknown error occurred' } };\n return response.status(500).send(data);\n } catch (error) {\n const message = error.name === 'AbortError'\n ? 'Request was aborted by the client.'\n : (error.message || 'An unknown network error occurred.');\n return response.status(500).send({ error: { message, ...error } });\n }\n}\n\nexport const router = express.Router();\n\nrouter.post('/status', async function (request, statusResponse) {\n try {\n if (!request.body) return statusResponse.sendStatus(400);\n\n let apiUrl = '';\n let apiKey = '';\n let headers = {};\n let queryParams = {};\n\n if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.OPENAI) {\n apiUrl = new URL(request.body.reverse_proxy || API_OPENAI).toString();\n apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.OPENAI);\n headers = {};\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.OPENROUTER) {\n apiUrl = 'https://openrouter.ai/api/v1';\n apiKey = readSecret(request.user.directories, SECRET_KEYS.OPENROUTER);\n // OpenRouter needs to pass the Referer and X-Title: https://openrouter.ai/docs#requests\n headers = { ...OPENROUTER_HEADERS };\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.MISTRALAI) {\n apiUrl = new URL(request.body.reverse_proxy || API_MISTRAL).toString();\n apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MISTRALAI);\n headers = {};\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.CUSTOM) {\n apiUrl = request.body.custom_url;\n apiKey = readSecret(request.user.directories, SECRET_KEYS.CUSTOM);\n headers = {};\n mergeObjectWithYaml(headers, request.body.custom_include_headers);\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.COHERE) {\n apiUrl = API_COHERE_V1;\n apiKey = readSecret(request.user.directories, SECRET_KEYS.COHERE);\n headers = {};\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.CHUTES) {\n apiUrl = API_CHUTES;\n apiKey = readSecret(request.user.directories, SECRET_KEYS.CHUTES);\n headers = {};\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.ELECTRONHUB) {\n apiUrl = API_ELECTRONHUB;\n apiKey = readSecret(request.user.directories, SECRET_KEYS.ELECTRONHUB);\n headers = {};\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.NANOGPT) {\n apiUrl = API_NANOGPT;\n apiKey = readSecret(request.user.directories, SECRET_KEYS.NANOGPT);\n headers = {};\n queryParams = { detailed: true };\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.DEEPSEEK) {\n apiUrl = new URL(request.body.reverse_proxy || API_DEEPSEEK.replace('/beta', '')).toString();\n apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK);\n headers = {};\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.XAI) {\n apiUrl = new URL(request.body.reverse_proxy || API_XAI).toString();\n apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.XAI);\n headers = {};\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.AIMLAPI) {\n apiUrl = API_AIMLAPI;\n apiKey = readSecret(request.user.directories, SECRET_KEYS.AIMLAPI);\n headers = { ...AIMLAPI_HEADERS };\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.POLLINATIONS) {\n apiUrl = 'https://text.pollinations.ai';\n apiKey = 'NONE';\n headers = {};\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.GROQ) {\n apiUrl = API_GROQ;\n apiKey = readSecret(request.user.directories, SECRET_KEYS.GROQ);\n headers = {};\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.COMETAPI) {\n apiUrl = API_COMETAPI;\n apiKey = readSecret(request.user.directories, SECRET_KEYS.COMETAPI);\n headers = {};\n throw new Error('This provider is temporarily disabled.');\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.MOONSHOT) {\n apiUrl = API_MOONSHOT;\n apiKey = readSecret(request.user.directories, SECRET_KEYS.MOONSHOT);\n headers = {};\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.FIREWORKS) {\n apiUrl = API_FIREWORKS;\n apiKey = readSecret(request.user.directories, SECRET_KEYS.FIREWORKS);\n headers = {};\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.MAKERSUITE) {\n apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MAKERSUITE);\n apiUrl = trimTrailingSlash(request.body.reverse_proxy || API_MAKERSUITE);\n const apiVersion = getConfigValue('gemini.apiVersion', 'v1beta');\n const modelsUrl = !apiKey && request.body.reverse_proxy\n ? `${apiUrl}/${apiVersion}/models`\n : `${apiUrl}/${apiVersion}/models?key=${apiKey}`;\n\n if (!apiKey && !request.body.reverse_proxy) {\n console.warn('Google AI Studio API key is missing.');\n return statusResponse.status(400).send({ error: true });\n }\n\n try {\n const response = await fetch(modelsUrl);\n\n if (response.ok) {\n /** @type {any} */\n const data = await response.json();\n // Transform Google AI Studio models to OpenAI format\n const models = data.models\n ?.filter(model => model.supportedGenerationMethods?.includes('generateContent'))\n ?.map(model => ({\n id: model.name.replace('models/', ''),\n })) || [];\n\n console.info('Available Google AI Studio models:', models.map(m => m.id));\n return statusResponse.send({ data: models });\n } else {\n console.warn('Google AI Studio models endpoint failed:', response.status, response.statusText);\n return statusResponse.send({ error: true, bypass: true, data: { data: [] } });\n }\n } catch (error) {\n console.error('Error fetching Google AI Studio models:', error);\n return statusResponse.send({ error: true, bypass: true, data: { data: [] } });\n }\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.AZURE_OPENAI) {\n const { azure_base_url, azure_deployment_name, azure_api_version } = request.body;\n const apiKey = readSecret(request.user.directories, SECRET_KEYS.AZURE_OPENAI);\n\n // 1) Validate configuration from the frontend\n if (!apiKey || !azure_base_url || !azure_deployment_name || !azure_api_version) {\n console.warn('Azure OpenAI status check failed: missing config from frontend.');\n return statusResponse.status(400).send({ error: true, message: 'Azure configuration is incomplete.' });\n }\n // 2) Build URLs using the URL API for consistency and robustness.\n const modelsUrl = new URL('/openai/models', azure_base_url);\n modelsUrl.searchParams.set('api-version', azure_api_version);\n\n const chatUrl = new URL(`/openai/deployments/${azure_deployment_name}/chat/completions`, azure_base_url);\n chatUrl.searchParams.set('api-version', azure_api_version);\n\n // Map common status codes to user-friendly error messages\n const azureStatusErrorMap = {\n 400: 'API version may be invalid for this resource.',\n 401: 'Invalid API key or insufficient permissions.',\n 403: 'Invalid API key or insufficient permissions.',\n 404: 'Endpoint URL appears incorrect (404).',\n };\n\n try {\n // ---- A) GET /models: fast sanity check for endpoint + api key + api version ----\n const apiConfigTest = await fetch(modelsUrl, {\n method: 'GET',\n headers: { 'api-key': apiKey, 'Accept': 'application/json' },\n });\n\n if (!apiConfigTest.ok) {\n let errText = '';\n try { errText = await apiConfigTest.text(); } catch { /* response body may be empty */ }\n\n console.warn('Azure OpenAI GET /models failed:', apiConfigTest.status, apiConfigTest.statusText, errText || '');\n\n const defaultMessage = `Azure Models endpoint error: ${apiConfigTest.statusText}`;\n const message = azureStatusErrorMap[apiConfigTest.status] ?? defaultMessage;\n return statusResponse.status(apiConfigTest.status).send({ error: true, message });\n }\n\n // ---- B) POST /chat/completions: verify deployment + read underlying model ID ----\n // Small, deterministic probe to minimize cost/latency\n const modelPayload = {\n messages: [{ role: 'user', content: 'Say word Hi' }],\n stream: false,\n max_completion_tokens: 5,\n };\n\n const modelRequest = await fetch(chatUrl, {\n method: 'POST',\n headers: { 'api-key': apiKey, 'Content-Type': 'application/json', 'Accept': 'application/json' },\n body: JSON.stringify(modelPayload),\n });\n\n let modelResponse;\n try {\n modelResponse = await modelRequest.json();\n } catch {\n modelResponse = { raw: 'Failed to parse JSON response from chat completions probe.' };\n }\n\n const modelId = /** @type {any} */ (modelResponse)?.model;\n if (!modelId) {\n console.warn('Azure status check succeeded but could not find a model ID in the response.');\n console.debug('Azure Response Body:', modelResponse);\n // Keep a benign success to avoid UX disruption in the UI\n return statusResponse.send({ data: [] });\n }\n\n console.info(color.green('Azure OpenAI connection successful. Detected model:'), modelId);\n // Consistent response format: always an array of { id }\n return statusResponse.send({ data: [{ id: modelId }] });\n } catch (error) {\n console.error('Azure OpenAI status check connection error:', error);\n return statusResponse.status(500).send({ error: true, message: 'Failed to connect to the Azure endpoint.' });\n }\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.SILICONFLOW) {\n apiUrl = API_SILICONFLOW;\n apiKey = readSecret(request.user.directories, SECRET_KEYS.SILICONFLOW);\n headers = {};\n } else {\n console.warn('This chat completion source is not supported yet.');\n return statusResponse.status(400).send({ error: true });\n }\n\n if (!apiKey && !request.body.reverse_proxy && request.body.chat_completion_source !== CHAT_COMPLETION_SOURCES.CUSTOM) {\n console.warn('Chat Completion API key is missing.');\n return statusResponse.status(400).send({ error: true });\n }\n\n const modelsUrl = new URL(urlJoin(apiUrl, '/models'));\n Object.keys(queryParams).forEach(key => {\n modelsUrl.searchParams.append(key, queryParams[key]);\n });\n const response = await fetch(modelsUrl, {\n method: 'GET',\n headers: {\n 'Authorization': 'Bearer ' + apiKey,\n ...headers,\n },\n });\n\n if (response.ok) {\n /** @type {any} */\n let data = await response.json();\n\n if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.POLLINATIONS && Array.isArray(data)) {\n data = { data: data.map(model => ({ id: model.name, ...model })) };\n }\n\n if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.CHUTES && Array.isArray(data?.data)) {\n data.data = data.data\n .filter(model => model?.id)\n .map(model => {\n if (model.pricing?.prompt !== undefined && model.pricing?.completion !== undefined) {\n return {\n ...model,\n pricing: {\n ...model.pricing,\n input: model.pricing.prompt,\n output: model.pricing.completion,\n },\n };\n }\n return model;\n });\n }\n\n statusResponse.send(data);\n\n if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.COHERE && Array.isArray(data?.models)) {\n data.data = data.models.map(model => ({ id: model.name, ...model }));\n }\n\n if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.OPENROUTER && Array.isArray(data?.data)) {\n let models = [];\n\n data.data.forEach(model => {\n const context_length = model.context_length;\n const tokens_dollar = Number(1 / (1000 * model.pricing?.prompt));\n const tokens_rounded = (Math.round(tokens_dollar * 1000) / 1000).toFixed(0);\n models[model.id] = {\n tokens_per_dollar: tokens_rounded + 'k',\n context_length: context_length,\n };\n });\n\n console.info('Available OpenRouter models:', models);\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.MISTRALAI) {\n const models = data?.data;\n console.info(models);\n } else {\n const models = data?.data;\n\n if (Array.isArray(models)) {\n const modelIds = models.filter(x => x && typeof x === 'object').map(x => x.id).sort();\n console.info('Available models:', modelIds);\n } else {\n console.warn('Chat Completion endpoint did not return a list of models.');\n }\n }\n }\n else {\n console.error('Chat Completion status check failed. Either Access Token is incorrect or API endpoint is down.');\n statusResponse.send({ error: true, data: { data: [] } });\n }\n } catch (e) {\n console.error(e);\n\n if (!statusResponse.headersSent) {\n statusResponse.send({ error: true });\n } else {\n statusResponse.end();\n }\n }\n});\n\nrouter.post('/bias', async function (request, response) {\n if (!request.body || !Array.isArray(request.body))\n return response.sendStatus(400);\n\n try {\n const result = {};\n const model = getTokenizerModel(String(request.query.model || ''));\n\n // no bias for claude\n if (model == 'claude') {\n return response.send(result);\n }\n\n let encodeFunction;\n\n if (sentencepieceTokenizers.includes(model)) {\n const tokenizer = getSentencepiceTokenizer(model);\n const instance = await tokenizer?.get();\n if (!instance) {\n console.error('Tokenizer not initialized:', model);\n return response.send({});\n }\n encodeFunction = (text) => new Uint32Array(instance.encodeIds(text));\n } else if (webTokenizers.includes(model)) {\n const tokenizer = getWebTokenizer(model);\n const instance = await tokenizer?.get();\n if (!instance) {\n console.warn('Tokenizer not initialized:', model);\n return response.send({});\n }\n encodeFunction = (text) => new Uint32Array(instance.encode(text));\n } else {\n const tokenizer = getTiktokenTokenizer(model);\n encodeFunction = (tokenizer.encode.bind(tokenizer));\n }\n\n for (const entry of request.body) {\n if (!entry || !entry.text) {\n continue;\n }\n\n try {\n const tokens = getEntryTokens(entry.text, encodeFunction);\n\n for (const token of tokens) {\n result[token] = entry.value;\n }\n } catch {\n console.warn('Tokenizer failed to encode:', entry.text);\n }\n }\n\n // not needed for cached tokenizers\n //tokenizer.free();\n return response.send(result);\n\n /**\n * Gets tokenids for a given entry\n * @param {string} text Entry text\n * @param {(string) => Uint32Array} encode Function to encode text to token ids\n * @returns {Uint32Array} Array of token ids\n */\n function getEntryTokens(text, encode) {\n // Get raw token ids from JSON array\n if (text.trim().startsWith('[') && text.trim().endsWith(']')) {\n try {\n const json = JSON.parse(text);\n if (Array.isArray(json) && json.every(x => typeof x === 'number')) {\n return new Uint32Array(json);\n }\n } catch {\n // ignore\n }\n }\n\n // Otherwise, get token ids from tokenizer\n return encode(text);\n }\n } catch (error) {\n console.error(error);\n return response.send({});\n }\n});\n\nrouter.post('/generate', async function (request, response) {\n try {\n if (!request.body) return response.status(400).send({ error: true });\n\n const postProcessingType = request.body.custom_prompt_post_processing;\n if (Array.isArray(request.body.messages) && postProcessingType) {\n console.info('Applying custom prompt post-processing of type', postProcessingType);\n request.body.messages = postProcessPrompt(\n request.body.messages,\n postProcessingType,\n getPromptNames(request));\n }\n\n if (request.body.json_schema?.value) {\n request.body.json_schema.value = flattenSchema(request.body.json_schema.value, request.body.chat_completion_source);\n }\n\n switch (request.body.chat_completion_source) {\n case CHAT_COMPLETION_SOURCES.CLAUDE: return await sendClaudeRequest(request, response);\n case CHAT_COMPLETION_SOURCES.AI21: return await sendAI21Request(request, response);\n case CHAT_COMPLETION_SOURCES.MAKERSUITE: return await sendMakerSuiteRequest(request, response);\n case CHAT_COMPLETION_SOURCES.VERTEXAI: return await sendMakerSuiteRequest(request, response);\n case CHAT_COMPLETION_SOURCES.MISTRALAI: return await sendMistralAIRequest(request, response);\n case CHAT_COMPLETION_SOURCES.COHERE: return await sendCohereRequest(request, response);\n case CHAT_COMPLETION_SOURCES.DEEPSEEK: return await sendDeepSeekRequest(request, response);\n case CHAT_COMPLETION_SOURCES.AIMLAPI: return await sendAimlapiRequest(request, response);\n case CHAT_COMPLETION_SOURCES.XAI: return await sendXaiRequest(request, response);\n case CHAT_COMPLETION_SOURCES.CHUTES: return await sendChutesRequest(request, response);\n case CHAT_COMPLETION_SOURCES.ELECTRONHUB: return await sendElectronHubRequest(request, response);\n case CHAT_COMPLETION_SOURCES.AZURE_OPENAI: return await sendAzureOpenAIRequest(request, response);\n }\n\n let apiUrl;\n let apiKey;\n let headers;\n let bodyParams;\n const isTextCompletion = Boolean(request.body.model && TEXT_COMPLETION_MODELS.includes(request.body.model)) || typeof request.body.messages === 'string';\n\n if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.OPENAI) {\n apiUrl = new URL(request.body.reverse_proxy || API_OPENAI).toString();\n apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.OPENAI);\n headers = {};\n bodyParams = {\n logprobs: request.body.logprobs,\n top_logprobs: undefined,\n };\n\n // Adjust logprobs params for Chat Completions API, which expects { top_logprobs: number; logprobs: boolean; }\n if (!isTextCompletion && bodyParams.logprobs > 0) {\n bodyParams.top_logprobs = bodyParams.logprobs;\n bodyParams.logprobs = true;\n }\n\n if (getConfigValue('openai.randomizeUserId', false, 'boolean')) {\n bodyParams['user'] = uuidv4();\n }\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.OPENROUTER) {\n apiUrl = 'https://openrouter.ai/api/v1';\n apiKey = readSecret(request.user.directories, SECRET_KEYS.OPENROUTER);\n // OpenRouter needs to pass the Referer and X-Title: https://openrouter.ai/docs#requests\n headers = { ...OPENROUTER_HEADERS };\n bodyParams = {\n 'transforms': getOpenRouterTransforms(request),\n 'plugins': getOpenRouterPlugins(request),\n 'include_reasoning': Boolean(request.body.include_reasoning),\n };\n\n if (request.body.min_p !== undefined) {\n bodyParams['min_p'] = request.body.min_p;\n }\n\n if (request.body.top_a !== undefined) {\n bodyParams['top_a'] = request.body.top_a;\n }\n\n if (request.body.repetition_penalty !== undefined) {\n bodyParams['repetition_penalty'] = request.body.repetition_penalty;\n }\n\n if (Array.isArray(request.body.provider) && request.body.provider.length > 0) {\n bodyParams['provider'] = {\n allow_fallbacks: request.body.allow_fallbacks ?? true,\n order: request.body.provider ?? [],\n };\n }\n\n if (request.body.use_fallback) {\n bodyParams['route'] = 'fallback';\n }\n\n if (request.body.reasoning_effort) {\n bodyParams['reasoning'] = { effort: request.body.reasoning_effort };\n }\n\n if (request.body.verbosity) {\n bodyParams['verbosity'] = request.body.verbosity;\n }\n\n if (request.body.json_schema) {\n bodyParams['response_format'] = {\n type: 'json_schema',\n json_schema: {\n name: request.body.json_schema.name,\n strict: request.body.json_schema.strict ?? true,\n schema: request.body.json_schema.value,\n },\n };\n }\n\n const isClaude = /^anthropic\\/claude/.test(request.body.model);\n const isGemini = /google\\/gemini/.test(request.body.model);\n const isCacheableGemini = isGemini && await isOpenRouterModelCacheable(request.body.model);\n const enableGeminiSystemPromptCache = getConfigValue('gemini.enableSystemPromptCache', false, 'boolean');\n\n if (Array.isArray(request.body.messages)) {\n embedOpenRouterMedia(request.body.messages);\n addOpenRouterSignatures(request.body.messages, request.body.model);\n\n if (isClaude) {\n if (enableSystemPromptCache) {\n cachingSystemPromptForOpenRouter(request.body.messages, cacheTTL);\n }\n\n if (cachingAtDepth !== -1) {\n cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth, cacheTTL);\n }\n }\n\n if (isCacheableGemini && enableGeminiSystemPromptCache) {\n cachingSystemPromptForOpenRouter(request.body.messages);\n }\n }\n\n if (isGemini) {\n bodyParams['safety_settings'] = GEMINI_SAFETY;\n }\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.CUSTOM) {\n apiUrl = request.body.custom_url;\n apiKey = readSecret(request.user.directories, SECRET_KEYS.CUSTOM);\n headers = {};\n bodyParams = {\n logprobs: request.body.logprobs,\n top_logprobs: undefined,\n };\n\n // Adjust logprobs params for Chat Completions API, which expects { top_logprobs: number; logprobs: boolean; }\n if (!isTextCompletion && bodyParams.logprobs > 0) {\n bodyParams.top_logprobs = bodyParams.logprobs;\n bodyParams.logprobs = true;\n }\n\n mergeObjectWithYaml(bodyParams, request.body.custom_include_body);\n mergeObjectWithYaml(headers, request.body.custom_include_headers);\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.PERPLEXITY) {\n apiUrl = API_PERPLEXITY;\n apiKey = readSecret(request.user.directories, SECRET_KEYS.PERPLEXITY);\n headers = {};\n bodyParams = {\n reasoning_effort: request.body.reasoning_effort,\n };\n request.body.messages = postProcessPrompt(request.body.messages, PROMPT_PROCESSING_TYPE.STRICT, getPromptNames(request));\n if (request.body.json_schema) {\n bodyParams['response_format'] = {\n type: 'json_schema',\n json_schema: {\n schema: request.body.json_schema.value,\n },\n };\n }\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.GROQ) {\n apiUrl = API_GROQ;\n apiKey = readSecret(request.user.directories, SECRET_KEYS.GROQ);\n headers = {};\n bodyParams = {};\n if (request.body.json_schema) {\n bodyParams['response_format'] = {\n type: 'json_schema',\n json_schema: {\n name: request.body.json_schema.name,\n description: request.body.json_schema.description,\n schema: request.body.json_schema.value,\n strict: request.body.json_schema.strict ?? true,\n },\n };\n }\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.FIREWORKS) {\n apiUrl = API_FIREWORKS;\n apiKey = readSecret(request.user.directories, SECRET_KEYS.FIREWORKS);\n headers = {};\n bodyParams = {};\n if (request.body.json_schema) {\n bodyParams['response_format'] = {\n type: 'json_schema',\n json_schema: {\n name: request.body.json_schema.name,\n description: request.body.json_schema.description,\n schema: request.body.json_schema.value,\n strict: request.body.json_schema.strict ?? true,\n },\n };\n }\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.NANOGPT) {\n apiUrl = API_NANOGPT;\n apiKey = readSecret(request.user.directories, SECRET_KEYS.NANOGPT);\n headers = {};\n bodyParams = {};\n if (request.body.enable_web_search && !/:online$/.test(request.body.model)) {\n request.body.model = `${request.body.model}:online`;\n }\n if (request.body.min_p !== undefined) {\n bodyParams['min_p'] = request.body.min_p;\n }\n if (request.body.top_a !== undefined) {\n bodyParams['top_a'] = request.body.top_a;\n }\n if (request.body.repetition_penalty !== undefined) {\n bodyParams['repetition_penalty'] = request.body.repetition_penalty;\n }\n\n const isClaude = /^claude-/.test(request.body.model);\n if (enableSystemPromptCache && isClaude) {\n bodyParams['cache_control'] = {\n 'enabled': true,\n 'ttl': cacheTTL,\n };\n }\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.POLLINATIONS) {\n apiUrl = API_POLLINATIONS;\n apiKey = 'NONE';\n headers = {\n 'Authorization': '',\n };\n bodyParams = {\n reasoning_effort: request.body.reasoning_effort,\n private: true,\n referrer: 'sillytavern',\n seed: request.body.seed ?? Math.floor(Math.random() * 99999999),\n };\n if (request.body.json_schema) {\n setJsonObjectFormat(bodyParams, request.body.messages, request.body.json_schema);\n }\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.MOONSHOT) {\n apiUrl = API_MOONSHOT;\n apiKey = readSecret(request.user.directories, SECRET_KEYS.MOONSHOT);\n headers = {};\n bodyParams = {};\n request.body.json_schema\n ? setJsonObjectFormat(bodyParams, request.body.messages, request.body.json_schema)\n : addAssistantPrefix(request.body.messages, [], 'partial');\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.COMETAPI) {\n apiUrl = API_COMETAPI;\n apiKey = readSecret(request.user.directories, SECRET_KEYS.COMETAPI);\n headers = {};\n bodyParams = {\n reasoning_effort: request.body.reasoning_effort,\n };\n throw new Error('This provider is temporarily disabled.');\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.ZAI) {\n apiUrl = request.body.zai_endpoint === ZAI_ENDPOINT.CODING ? API_ZAI_CODING : API_ZAI_COMMON;\n apiKey = readSecret(request.user.directories, SECRET_KEYS.ZAI);\n headers = {\n 'Accept-Language': 'en-US,en',\n };\n bodyParams = {\n thinking: {\n type: request.body.include_reasoning ? 'enabled' : 'disabled',\n },\n };\n if (request.body.json_schema) {\n setJsonObjectFormat(bodyParams, request.body.messages, request.body.json_schema);\n }\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.SILICONFLOW) {\n apiUrl = API_SILICONFLOW;\n apiKey = readSecret(request.user.directories, SECRET_KEYS.SILICONFLOW);\n headers = {};\n bodyParams = {};\n if (request.body.json_schema) {\n setJsonObjectFormat(bodyParams, request.body.messages, request.body.json_schema);\n }\n } else {\n console.warn('This chat completion source is not supported yet.');\n return response.status(400).send({ error: true });\n }\n\n // A few of OpenAIs reasoning models support reasoning effort\n if (request.body.reasoning_effort && [CHAT_COMPLETION_SOURCES.CUSTOM, CHAT_COMPLETION_SOURCES.OPENAI].includes(request.body.chat_completion_source)) {\n if (OPENAI_REASONING_EFFORT_MODELS.includes(request.body.model)) {\n bodyParams['reasoning_effort'] = OPENAI_REASONING_EFFORT_MAP[request.body.reasoning_effort] ?? request.body.reasoning_effort;\n }\n }\n\n if (request.body.verbosity && [CHAT_COMPLETION_SOURCES.CUSTOM, CHAT_COMPLETION_SOURCES.OPENAI].includes(request.body.chat_completion_source)) {\n if (OPENAI_VERBOSITY_MODELS.test(request.body.model)) {\n bodyParams['verbosity'] = request.body.verbosity;\n }\n }\n\n if (!apiKey && !request.body.reverse_proxy && request.body.chat_completion_source !== CHAT_COMPLETION_SOURCES.CUSTOM) {\n console.warn('OpenAI API key is missing.');\n return response.status(400).send({ error: true });\n }\n\n // Add custom stop sequences\n if (Array.isArray(request.body.stop) && request.body.stop.length > 0) {\n bodyParams['stop'] = request.body.stop;\n }\n\n const textPrompt = isTextCompletion ? convertTextCompletionPrompt(request.body.messages) : '';\n const endpointUrl = isTextCompletion && request.body.chat_completion_source !== CHAT_COMPLETION_SOURCES.OPENROUTER ?\n `${apiUrl}/completions` :\n `${apiUrl}/chat/completions`;\n\n const controller = new AbortController();\n request.socket.removeAllListeners('close');\n request.socket.on('close', function () {\n controller.abort();\n });\n\n if (!isTextCompletion && Array.isArray(request.body.tools) && request.body.tools.length > 0) {\n bodyParams['tools'] = request.body.tools;\n bodyParams['tool_choice'] = request.body.tool_choice;\n }\n\n if (request.body.json_schema && !bodyParams['response_format']) {\n bodyParams['response_format'] = {\n type: 'json_schema',\n json_schema: {\n name: request.body.json_schema.name,\n strict: request.body.json_schema.strict ?? true,\n schema: request.body.json_schema.value,\n },\n };\n }\n\n const requestBody = {\n 'messages': isTextCompletion === false ? request.body.messages : undefined,\n 'prompt': isTextCompletion === true ? textPrompt : undefined,\n 'model': request.body.model,\n 'temperature': request.body.temperature,\n 'max_tokens': request.body.max_tokens,\n 'max_completion_tokens': request.body.max_completion_tokens,\n 'stream': request.body.stream,\n 'presence_penalty': request.body.presence_penalty,\n 'frequency_penalty': request.body.frequency_penalty,\n 'top_p': request.body.top_p,\n 'top_k': request.body.top_k,\n 'stop': isTextCompletion === false ? request.body.stop : undefined,\n 'logit_bias': request.body.logit_bias,\n 'seed': request.body.seed,\n 'n': request.body.n,\n ...bodyParams,\n };\n\n if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.CUSTOM) {\n excludeKeysByYaml(requestBody, request.body.custom_exclude_body);\n }\n\n /** @type {import('node-fetch').RequestInit} */\n const config = {\n method: 'post',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer ' + apiKey,\n ...headers,\n },\n body: JSON.stringify(requestBody),\n signal: controller.signal,\n };\n\n console.debug('Chat Completion request:', requestBody);\n\n const fetchResponse = await fetch(endpointUrl, config);\n\n if (request.body.stream) {\n console.info('Streaming request in progress');\n return forwardFetchResponse(fetchResponse, response);\n }\n\n if (fetchResponse.ok) {\n /** @type {any} */\n const json = await fetchResponse.json();\n console.debug('Chat Completion response:', json);\n return response.send(json);\n } else {\n const responseText = await fetchResponse.text();\n const errorData = tryParse(responseText);\n\n const message = fetchResponse.statusText || 'Unknown error occurred';\n const quota_error = fetchResponse.status === 429 && errorData?.error?.type === 'insufficient_quota';\n console.error('Chat completion request error: ', message, responseText);\n\n if (!response.headersSent) {\n response.send({ error: { message }, quota_error: quota_error });\n } else if (!response.writableEnded) {\n response.write(responseText);\n } else {\n response.end();\n }\n }\n } catch (error) {\n console.error('Generation failed', error);\n const message = error.code === 'ECONNREFUSED'\n ? `Connection refused: ${error.message}`\n : error.message || 'Unknown error occurred';\n\n if (!response.headersSent) {\n response.status(502).send({ error: { message, ...error } });\n } else {\n response.end();\n }\n }\n});\n\nconst multimodalModels = express.Router();\n\nmultimodalModels.post('/pollinations', async (_req, res) => {\n try {\n const response = await fetch('https://text.pollinations.ai/models');\n\n if (!response.ok) {\n return res.json([]);\n }\n\n /** @type {any} */\n const data = await response.json();\n\n if (!Array.isArray(data)) {\n return res.json([]);\n }\n\n const multimodalModels = data.filter(m => m?.vision).map(m => m.name);\n return res.json(multimodalModels);\n } catch (error) {\n console.error(error);\n return res.sendStatus(500);\n }\n});\n\nmultimodalModels.post('/aimlapi', async (_req, res) => {\n try {\n const response = await fetch('https://api.aimlapi.com/v1/models');\n\n if (!response.ok) {\n return res.json([]);\n }\n\n /** @type {any} */\n const data = await response.json();\n\n if (!Array.isArray(data?.data)) {\n return res.json([]);\n }\n\n const multimodalModels = data.data.filter(m => m?.features?.includes('openai/chat-completion.vision')).map(m => m.id);\n return res.json(multimodalModels);\n } catch (error) {\n console.error(error);\n return res.sendStatus(500);\n }\n});\n\nmultimodalModels.post('/nanogpt', async (_req, res) => {\n try {\n const response = await fetch('https://nano-gpt.com/api/v1/models?detailed=true');\n\n if (!response.ok) {\n return res.json([]);\n }\n\n /** @type {any} */\n const data = await response.json();\n\n if (!Array.isArray(data?.data)) {\n return res.json([]);\n }\n\n const multimodalModels = data.data.filter(m => m?.capabilities?.vision).map(m => m.id);\n return res.json(multimodalModels);\n } catch (error) {\n console.error(error);\n return res.sendStatus(500);\n }\n});\n\nmultimodalModels.post('/electronhub', async (_req, res) => {\n try {\n const response = await fetch('https://api.electronhub.ai/v1/models');\n\n if (!response.ok) {\n return res.json([]);\n }\n\n /** @type {any} */\n const data = await response.json();\n const multimodalModels = data.data.filter(m => m.metadata?.vision).map(m => m.id);\n return res.json(multimodalModels);\n } catch (error) {\n console.error(error);\n return res.sendStatus(500);\n }\n});\n\nmultimodalModels.post('/chutes', async (req, res) => {\n try {\n const key = readSecret(req.user.directories, SECRET_KEYS.CHUTES);\n\n if (!key) {\n return res.json([]);\n }\n\n const response = await fetch('https://llm.chutes.ai/v1/models', {\n headers: {\n 'Authorization': `Bearer ${key}`,\n },\n });\n\n if (!response.ok) {\n return res.json([]);\n }\n\n const data = await response.json();\n\n const modelsData = /** @type {{object: string, data: Array<{id: string, input_modalities?: string[]}>}} */ (data);\n const multimodalModels = modelsData.data\n .filter(m => m.input_modalities?.includes('image'))\n .map(m => m.id);\n return res.json(multimodalModels);\n } catch (error) {\n console.error(error);\n return res.sendStatus(500);\n }\n});\n\nmultimodalModels.post('/mistral', async (req, res) => {\n try {\n const key = readSecret(req.user.directories, SECRET_KEYS.MISTRALAI);\n\n if (!key) {\n return res.json([]);\n }\n\n const response = await fetch('https://api.mistral.ai/v1/models', {\n headers: {\n 'Authorization': `Bearer ${key}`,\n },\n });\n\n if (!response.ok) {\n return res.json([]);\n }\n\n /** @type {any} */\n const data = await response.json();\n const multimodalModels = data.data.filter(m => m.capabilities?.vision).map(m => m.id);\n return res.json(multimodalModels);\n } catch (error) {\n console.error(error);\n return res.sendStatus(500);\n }\n});\n\nmultimodalModels.post('/xai', async (req, res) => {\n try {\n const key = readSecret(req.user.directories, SECRET_KEYS.XAI);\n\n if (!key) {\n return res.json([]);\n }\n\n // xAI's /models endpoint doesn't return modality info, so we must use /language-models instead\n const response = await fetch('https://api.x.ai/v1/language-models', {\n headers: {\n 'Authorization': `Bearer ${key}`,\n },\n });\n\n if (!response.ok) {\n return res.json([]);\n }\n\n /** @type {any} */\n const data = await response.json();\n const multimodalModels = data.models.filter(m => m.input_modalities?.includes('image')).map(m => m.id);\n if (!multimodalModels.includes('grok-4-0709')) {\n // The endpoint says it doesn't support images, but it does\n multimodalModels.push('grok-4-0709');\n }\n return res.json(multimodalModels);\n } catch (error) {\n console.error(error);\n return res.sendStatus(500);\n }\n});\n\nrouter.use('/multimodal-models', multimodalModels);\n\nrouter.post('/process', async function (request, response) {\n try {\n if (!Array.isArray(request.body.messages)) {\n return response.status(400).send({ error: 'Invalid messages format' });\n }\n\n if (!Object.values(PROMPT_PROCESSING_TYPE).includes(request.body.type)) {\n return response.status(400).send({ error: 'Unknown processing type' });\n }\n\n const messages = postProcessPrompt(request.body.messages, request.body.type, getPromptNames(request));\n return response.send({ messages });\n } catch (error) {\n console.error(error);\n return response.sendStatus(500);\n }\n});\n"}, "31b55042120e9a91e924ab6fa2259a265640edb6": {"t": 1785766940.611659, "n": 107141, "h": "a1b714129eb60c76", "c": "import process from 'node:process';\nimport util from 'node:util';\nimport express from 'express';\nimport fetch from 'node-fetch';\nimport urlJoin from 'url-join';\n\nimport {\n AIMLAPI_HEADERS,\n AZURE_OPENAI_KEYS,\n CHAT_COMPLETION_SOURCES,\n GEMINI_SAFETY,\n OPENAI_REASONING_EFFORT_MAP,\n OPENAI_REASONING_EFFORT_MODELS,\n OPENAI_VERBOSITY_MODELS,\n OPENROUTER_HEADERS,\n VERTEX_SAFETY,\n ZAI_ENDPOINT,\n} from '../../constants.js';\nimport {\n forwardFetchResponse,\n getConfigValue,\n tryParse,\n uuidv4,\n mergeObjectWithYaml,\n excludeKeysByYaml,\n color,\n trimTrailingSlash,\n flattenSchema,\n} from '../../util.js';\nimport {\n convertClaudeMessages,\n convertGooglePrompt,\n convertTextCompletionPrompt,\n convertCohereMessages,\n convertMistralMessages,\n convertAI21Messages,\n convertXAIMessages,\n cachingAtDepthForOpenRouterClaude,\n cachingAtDepthForClaude,\n getPromptNames,\n calculateClaudeBudgetTokens,\n calculateGoogleBudgetTokens,\n postProcessPrompt,\n PROMPT_PROCESSING_TYPE,\n addAssistantPrefix,\n embedOpenRouterMedia,\n addReasoningContentToToolCalls,\n cachingSystemPromptForOpenRouter,\n addOpenRouterSignatures,\n} from '../../prompt-converters.js';\n\nimport { readSecret, SECRET_KEYS } from '../secrets.js';\nimport {\n getTokenizerModel,\n getSentencepiceTokenizer,\n getTiktokenTokenizer,\n sentencepieceTokenizers,\n TEXT_COMPLETION_MODELS,\n webTokenizers,\n getWebTokenizer,\n} from '../tokenizers.js';\nimport { getVertexAIAuth, getProjectIdFromServiceAccount } from '../google.js';\n\nconst API_OPENAI = 'https://api.openai.com/v1';\nconst API_CLAUDE = 'https://api.anthropic.com/v1';\nconst API_MISTRAL = 'https://api.mistral.ai/v1';\nconst API_COHERE_V1 = 'https://api.cohere.ai/v1';\nconst API_COHERE_V2 = 'https://api.cohere.ai/v2';\nconst API_PERPLEXITY = 'https://api.perplexity.ai';\nconst API_GROQ = 'https://api.groq.com/openai/v1';\nconst API_MAKERSUITE = 'https://generativelanguage.googleapis.com';\nconst API_VERTEX_AI = 'https://us-central1-aiplatform.googleapis.com';\nconst API_AI21 = 'https://api.ai21.com/studio/v1';\nconst API_CHUTES = 'https://llm.chutes.ai/v1';\nconst API_ELECTRONHUB = 'https://api.electronhub.ai/v1';\nconst API_NANOGPT = 'https://nano-gpt.com/api/v1';\nconst API_DEEPSEEK = 'https://api.deepseek.com/beta';\nconst API_XAI = 'https://api.x.ai/v1';\nconst API_AIMLAPI = 'https://api.aimlapi.com/v1';\nconst API_POLLINATIONS = 'https://text.pollinations.ai/openai';\nconst API_MOONSHOT = 'https://api.moonshot.ai/v1';\nconst API_FIREWORKS = 'https://api.fireworks.ai/inference/v1';\nconst API_COMETAPI = 'https://api.cometapi.com/v1';\nconst API_ZAI_COMMON = 'https://api.z.ai/api/paas/v4';\nconst API_ZAI_CODING = 'https://api.z.ai/api/coding/paas/v4';\nconst API_SILICONFLOW = 'https://api.siliconflow.com/v1';\nconst API_OPENROUTER = 'https://openrouter.ai/api/v1';\n\n/**\n * Module-scoped Claude caching configuration values.\n */\nconst cacheTTL = getConfigValue('claude.extendedTTL', false, 'boolean') ? '1h' : '5m';\nconst enableSystemPromptCache = getConfigValue('claude.enableSystemPromptCache', false, 'boolean');\nconst cachingAtDepth = (() => {\n const value = getConfigValue('claude.cachingAtDepth', -1, 'number');\n return Number.isInteger(value) && value >= 0 ? value : -1;\n})();\n\n/**\n * Cache for cacheable (writing) OpenRouter model IDs.\n * @type {string[]}\n */\nconst openRouterCacheableModels = [];\n\n/**\n * Checks if an OpenRouter model supports prompt cache writing.\n * Uses a cache to avoid repeated API calls.\n * @param {string} modelId - The OpenRouter model ID\n * @returns {Promise} `true` if the model supports writing cache\n */\nasync function isOpenRouterModelCacheable(modelId) {\n if (openRouterCacheableModels.includes(modelId)) {\n return true;\n }\n\n try {\n const response = await fetch(`${API_OPENROUTER}/models`, {\n method: 'GET',\n headers: { 'Accept': 'application/json' },\n signal: AbortSignal.timeout(5000),\n });\n\n if (!response.ok) {\n console.warn(`OpenRouter models API returned ${response.status}: ${response.statusText}`);\n return false;\n }\n\n /** @type {any} */\n const data = await response.json();\n\n if (!Array.isArray(data?.data)) {\n console.warn('OpenRouter API response format unexpected');\n return false;\n }\n\n const model = data.data.find(m => m.id === modelId);\n const supportsCache = model?.pricing?.input_cache_write != null;\n\n if (supportsCache) {\n openRouterCacheableModels.push(modelId);\n }\n\n return supportsCache;\n } catch (error) {\n console.warn(`Failed to check OpenRouter cache support for ${modelId}:`, error.message);\n return false;\n }\n}\n\n/**\n * Gets OpenRouter transforms based on the request.\n * @param {import('express').Request} request Express request\n * @returns {string[] | undefined} OpenRouter transforms\n */\nfunction getOpenRouterTransforms(request) {\n switch (request.body.middleout) {\n case 'on':\n return ['middle-out'];\n case 'off':\n return [];\n case 'auto':\n return undefined;\n }\n}\n\n/**\n * Gets OpenRouter plugins based on the request.\n * @param {import('express').Request} request\n * @returns {any[]} OpenRouter plugins\n */\nfunction getOpenRouterPlugins(request) {\n const plugins = [];\n\n if (request.body.enable_web_search) {\n plugins.push({ 'id': 'web' });\n }\n\n return plugins;\n}\n\n/**\n * Hacky way to use JSON schema only if json_object format is supported.\n * @param {object} bodyParams Additional body parameters\n * @param {object[]} messages Array of messages\n * @param {object} jsonSchema JSON schema object\n */\nfunction setJsonObjectFormat(bodyParams, messages, jsonSchema) {\n bodyParams['response_format'] = {\n type: 'json_object',\n };\n const message = {\n role: 'user',\n content: `JSON schema for the response:\\n${JSON.stringify(jsonSchema.value, null, 4)}`,\n };\n messages.push(message);\n}\n\n/**\n * Sends a request to Claude API.\n * @param {express.Request} request Express request\n * @param {express.Response} response Express response\n */\nasync function sendClaudeRequest(request, response) {\n const apiUrl = new URL(request.body.reverse_proxy || API_CLAUDE).toString();\n const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.CLAUDE);\n const divider = '-'.repeat(process.stdout.columns);\n\n if (!apiKey) {\n console.warn(color.red(`Claude API key is missing.\\n${divider}`));\n return response.status(400).send({ error: true });\n }\n\n try {\n const controller = new AbortController();\n request.socket.removeAllListeners('close');\n request.socket.on('close', function () {\n controller.abort();\n });\n const additionalHeaders = {};\n const betaHeaders = ['output-128k-2025-02-19'];\n const useTools = Array.isArray(request.body.tools) && request.body.tools.length > 0;\n const useSystemPrompt = Boolean(request.body.use_sysprompt);\n const convertedPrompt = convertClaudeMessages(request.body.messages, request.body.assistant_prefill, useSystemPrompt, useTools, getPromptNames(request));\n const useThinking = /^claude-(3-7|opus-4|sonnet-4|haiku-4-5|opus-4-5)/.test(request.body.model);\n const useWebSearch = /^claude-(3-5|3-7|opus-4|sonnet-4|haiku-4-5|opus-4-5)/.test(request.body.model) && Boolean(request.body.enable_web_search);\n const isLimitedSampling = /^claude-(opus-4-1|sonnet-4-5|haiku-4-5|opus-4-5)/.test(request.body.model);\n const useVerbosity = /^claude-(opus-4-5)/.test(request.body.model);\n let fixThinkingPrefill = false;\n // Add custom stop sequences\n const stopSequences = [];\n if (Array.isArray(request.body.stop)) {\n stopSequences.push(...request.body.stop);\n }\n\n const requestBody = {\n /** @type {any} */ system: [],\n messages: convertedPrompt.messages,\n model: request.body.model,\n max_tokens: request.body.max_tokens,\n stop_sequences: stopSequences,\n temperature: request.body.temperature,\n top_p: request.body.top_p,\n top_k: request.body.top_k,\n stream: request.body.stream,\n };\n if (useSystemPrompt) {\n if (enableSystemPromptCache && Array.isArray(convertedPrompt.systemPrompt) && convertedPrompt.systemPrompt.length) {\n convertedPrompt.systemPrompt[convertedPrompt.systemPrompt.length - 1]['cache_control'] = { type: 'ephemeral', ttl: cacheTTL };\n }\n\n requestBody.system = convertedPrompt.systemPrompt;\n } else {\n delete requestBody.system;\n }\n if (useTools) {\n betaHeaders.push('tools-2024-05-16');\n requestBody.tool_choice = { type: request.body.tool_choice };\n requestBody.tools = request.body.tools\n .filter(tool => tool.type === 'function')\n .map(tool => tool.function)\n .map(fn => ({ name: fn.name, description: fn.description, input_schema: flattenSchema(fn.parameters, request.body.chat_completion_source) }));\n\n if (enableSystemPromptCache && requestBody.tools.length) {\n requestBody.tools[requestBody.tools.length - 1]['cache_control'] = { type: 'ephemeral', ttl: cacheTTL };\n }\n }\n\n // Structured output is a forced tool\n if (request.body.json_schema) {\n const jsonTool = {\n name: request.body.json_schema.name,\n description: request.body.json_schema.description || 'Well-formed JSON object',\n input_schema: request.body.json_schema.value,\n };\n requestBody.tools = [...(requestBody.tools || []), jsonTool];\n requestBody.tool_choice = { type: 'tool', name: request.body.json_schema.name };\n }\n\n if (useWebSearch) {\n const webSearchTool = [{\n 'type': 'web_search_20250305',\n 'name': 'web_search',\n }];\n requestBody.tools = [...webSearchTool, ...(requestBody.tools || [])];\n }\n\n if (cachingAtDepth !== -1) {\n cachingAtDepthForClaude(convertedPrompt.messages, cachingAtDepth, cacheTTL);\n }\n\n if (enableSystemPromptCache || cachingAtDepth !== -1) {\n betaHeaders.push('prompt-caching-2024-07-31');\n betaHeaders.push('extended-cache-ttl-2025-04-11');\n }\n\n if (isLimitedSampling) {\n if (requestBody.top_p < 1) {\n delete requestBody.temperature;\n } else {\n delete requestBody.top_p;\n }\n }\n\n const reasoningEffort = request.body.reasoning_effort;\n const budgetTokens = calculateClaudeBudgetTokens(requestBody.max_tokens, reasoningEffort, requestBody.stream);\n\n if (useThinking && Number.isInteger(budgetTokens)) {\n // No prefill when thinking\n fixThinkingPrefill = true;\n const minThinkTokens = 1024;\n if (requestBody.max_tokens <= minThinkTokens) {\n const newValue = requestBody.max_tokens + minThinkTokens;\n console.warn(color.yellow(`Claude thinking requires a minimum of ${minThinkTokens} response tokens.`));\n console.info(color.blue(`Increasing response length to ${newValue}.`));\n requestBody.max_tokens = newValue;\n }\n requestBody.thinking = {\n type: 'enabled',\n budget_tokens: budgetTokens,\n };\n\n // NO I CAN'T SILENTLY IGNORE THE TEMPERATURE.\n delete requestBody.temperature;\n delete requestBody.top_p;\n delete requestBody.top_k;\n }\n\n if (fixThinkingPrefill && convertedPrompt.messages.length && convertedPrompt.messages[convertedPrompt.messages.length - 1].role === 'assistant') {\n convertedPrompt.messages[convertedPrompt.messages.length - 1].role = 'user';\n }\n\n // Verbosity = 'effort' (same values as OpenAI)\n if (useVerbosity && request.body.verbosity) {\n betaHeaders.push('effort-2025-11-24');\n requestBody.output_config ??= {};\n requestBody.output_config.effort = request.body.verbosity;\n }\n\n if (betaHeaders.length) {\n additionalHeaders['anthropic-beta'] = betaHeaders.join(',');\n }\n\n console.debug('Claude request:', requestBody);\n\n const generateResponse = await fetch(apiUrl + '/messages', {\n method: 'POST',\n signal: controller.signal,\n body: JSON.stringify(requestBody),\n headers: {\n 'Content-Type': 'application/json',\n 'anthropic-version': '2023-06-01',\n 'x-api-key': apiKey,\n ...additionalHeaders,\n },\n });\n\n if (request.body.stream) {\n // Pipe remote SSE stream to Express response\n forwardFetchResponse(generateResponse, response);\n } else {\n if (!generateResponse.ok) {\n const generateResponseText = await generateResponse.text();\n console.warn(color.red(`Claude API returned error: ${generateResponse.status} ${generateResponse.statusText}\\n${generateResponseText}\\n${divider}`));\n return response.status(500).send({ error: true });\n }\n\n /** @type {any} */\n const generateResponseJson = await generateResponse.json();\n const responseText = generateResponseJson?.content?.[0]?.text || '';\n console.debug('Claude response:', generateResponseJson);\n\n // Wrap it back to OAI format + save the original content\n const reply = { choices: [{ 'message': { 'content': responseText } }], content: generateResponseJson.content };\n return response.send(reply);\n }\n } catch (error) {\n console.error(color.red(`Error communicating with Claude: ${error}\\n${divider}`));\n if (!response.headersSent) {\n return response.status(500).send({ error: true });\n }\n }\n}\n\n/**\n * Sends a request to Google AI API.\n * @param {express.Request} request Express request\n * @param {express.Response} response Express response\n */\nasync function sendMakerSuiteRequest(request, response) {\n const useVertexAi = request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.VERTEXAI;\n const apiName = useVertexAi ? 'Google Vertex AI' : 'Google AI Studio';\n let apiUrl;\n let apiKey;\n\n let authHeader;\n let authType;\n\n if (useVertexAi) {\n apiUrl = new URL(request.body.reverse_proxy || API_VERTEX_AI);\n\n try {\n const auth = await getVertexAIAuth(request);\n authHeader = auth.authHeader;\n authType = auth.authType;\n console.debug(`Using Vertex AI authentication type: ${authType}`);\n } catch (error) {\n console.warn(`${apiName} authentication failed: ${error.message}`);\n return response.status(400).send({ error: true, message: error.message });\n }\n } else {\n apiUrl = new URL(request.body.reverse_proxy || API_MAKERSUITE);\n apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MAKERSUITE);\n\n if (!request.body.reverse_proxy && !apiKey) {\n console.warn(`${apiName} API key is missing.`);\n return response.status(400).send({ error: true });\n }\n\n authHeader = `Bearer ${apiKey}`;\n authType = 'api_key';\n }\n\n const model = String(request.body.model);\n const stream = Boolean(request.body.stream);\n const enableWebSearch = Boolean(request.body.enable_web_search);\n const requestImages = Boolean(request.body.request_images);\n const reasoningEffort = String(request.body.reasoning_effort);\n const includeReasoning = Boolean(request.body.include_reasoning);\n const aspectRatio = String(request.body.request_image_aspect_ratio);\n const imageSize = String(request.body.request_image_resolution);\n const isGemma = model.includes('gemma');\n const isLearnLM = model.includes('learnlm');\n\n const responseMimeType = request.body.responseMimeType ?? (request.body.json_schema ? 'application/json' : undefined);\n const responseSchema = request.body.responseSchema ?? (request.body.json_schema ? request.body.json_schema.value : undefined);\n\n const generationConfig = {\n stopSequences: request.body.stop,\n candidateCount: 1,\n maxOutputTokens: request.body.max_tokens,\n temperature: request.body.temperature,\n topP: request.body.top_p,\n topK: request.body.top_k || undefined,\n responseMimeType: responseMimeType,\n responseSchema: responseSchema,\n seed: request.body.seed,\n };\n\n function getGeminiBody() {\n // #region UGLY MODEL LISTS AREA\n const imageGenerationModels = [\n 'gemini-2.0-flash-exp',\n 'gemini-2.0-flash-exp-image-generation',\n 'gemini-2.0-flash-preview-image-generation',\n 'gemini-2.5-flash-image-preview',\n 'gemini-2.5-flash-image',\n 'gemini-3-pro-image-preview',\n ];\n\n const isThinkingConfigModel = m => (/^gemini-2.5-(flash|pro)/.test(m) && !/-image(-preview)?$/.test(m)) || (/^gemini-3-(flash|pro)/.test(m));\n const isImageSizeModel = m => /^gemini-3/.test(m);\n\n const noSearchModels = [\n 'gemini-2.0-flash-lite',\n 'gemini-2.0-flash-lite-001',\n 'gemini-2.0-flash-lite-preview-02-05',\n 'gemini-robotics-er-1.5-preview',\n ];\n // #endregion\n\n if (!Array.isArray(generationConfig.stopSequences) || !generationConfig.stopSequences.length) {\n delete generationConfig.stopSequences;\n }\n\n const enableImageModality = requestImages && imageGenerationModels.includes(model);\n const enableImageConfig = enableImageModality && (aspectRatio || imageSize);\n if (enableImageModality) {\n generationConfig.responseModalities = ['text', 'image'];\n if (enableImageConfig) {\n generationConfig.imageConfig = {};\n if (imageSize && isImageSizeModel(model)) {\n generationConfig.imageConfig.imageSize = imageSize;\n }\n if (aspectRatio) {\n generationConfig.imageConfig.aspectRatio = aspectRatio;\n }\n }\n }\n\n const useSystemPrompt = !enableImageModality && !isGemma && request.body.use_sysprompt;\n\n const tools = [];\n const prompt = convertGooglePrompt(request.body.messages, model, useSystemPrompt, getPromptNames(request));\n const safetySettings = [...GEMINI_SAFETY, ...(useVertexAi ? VERTEX_SAFETY : [])];\n\n if (Array.isArray(request.body.tools) && request.body.tools.length > 0 && !enableImageModality && !isGemma) {\n const functionDeclarations = [];\n const customTools = [];\n for (const tool of request.body.tools) {\n if (tool.type === 'function') {\n if (tool.function.parameters?.$schema) {\n delete tool.function.parameters.$schema;\n }\n if (tool.function.parameters?.properties && Object.keys(tool.function.parameters.properties).length === 0) {\n delete tool.function.parameters;\n }\n functionDeclarations.push(tool.function);\n } else if (tool[tool.type]) {\n customTools.push({ [tool.type]: tool[tool.type] });\n }\n }\n if (functionDeclarations.length > 0) {\n tools.push({ function_declarations: functionDeclarations });\n }\n // Custom tools are only supported when no function calling is present\n if (functionDeclarations.length === 0 && customTools.length > 0) {\n tools.push(...customTools);\n }\n }\n\n if (enableWebSearch && !enableImageModality && !isGemma && !isLearnLM && !noSearchModels.includes(model)) {\n // Tool use with function calling is unsupported\n if (!tools.some(t => t.function_declarations)) {\n tools.push({ google_search: {} });\n }\n }\n\n if (isThinkingConfigModel(model)) {\n const thinkingConfig = { includeThoughts: includeReasoning };\n\n const thinkingBudget = calculateGoogleBudgetTokens(generationConfig.maxOutputTokens, reasoningEffort, model);\n if (typeof thinkingBudget === 'number' && Number.isInteger(thinkingBudget)) {\n thinkingConfig.thinkingBudget = thinkingBudget;\n }\n\n if (typeof thinkingBudget === 'string' && thinkingBudget.length > 0) {\n thinkingConfig.thinkingLevel = thinkingBudget;\n }\n\n // Vertex doesn't allow mixing disabled thinking with includeThoughts\n if (useVertexAi && thinkingBudget === 0 && thinkingConfig.includeThoughts) {\n console.info('Thinking budget is 0, but includeThoughts is true. Thoughts will not be included in the response.');\n thinkingConfig.includeThoughts = false;\n }\n\n generationConfig.thinkingConfig = thinkingConfig;\n }\n\n let body = {\n contents: prompt.contents,\n safetySettings: safetySettings,\n generationConfig: generationConfig,\n };\n\n if (useSystemPrompt && Array.isArray(prompt.system_instruction.parts) && prompt.system_instruction.parts.length) {\n body.systemInstruction = prompt.system_instruction;\n }\n\n if (tools.length) {\n body.tools = tools;\n\n const toolChoice = request.body.tool_choice;\n let functionCallingConfig;\n\n // Translate OpenAI's `tool_choice` to Gemini's `functionCallingConfig`\n if (typeof toolChoice === 'string') {\n switch (toolChoice) {\n case 'none':\n functionCallingConfig = { mode: 'NONE' };\n break;\n case 'required':\n functionCallingConfig = { mode: 'ANY' };\n break;\n case 'auto':\n functionCallingConfig = { mode: 'AUTO' };\n break;\n }\n } else if (typeof toolChoice === 'object' && toolChoice?.function?.name) {\n // Force a specific function call\n functionCallingConfig = {\n mode: 'ANY',\n allowedFunctionNames: [toolChoice.function.name],\n };\n }\n\n if (functionCallingConfig) {\n body.toolConfig = { functionCallingConfig };\n }\n }\n\n return body;\n }\n\n const body = getGeminiBody();\n console.debug(`${apiName} request:`, body);\n\n try {\n const controller = new AbortController();\n request.socket.removeAllListeners('close');\n request.socket.on('close', function () {\n controller.abort();\n });\n\n const apiVersion = getConfigValue('gemini.apiVersion', 'v1beta');\n const responseType = (stream ? 'streamGenerateContent' : 'generateContent');\n\n let url;\n let headers = {\n 'Content-Type': 'application/json',\n };\n\n if (useVertexAi) {\n if (authType === 'express') {\n // For Express mode (API key authentication), use the key parameter\n const keyParam = authHeader.replace('Bearer ', '');\n const region = request.body.vertexai_region || 'us-central1';\n const projectId = request.body.vertexai_express_project_id;\n const baseUrl = region === 'global'\n ? 'https://aiplatform.googleapis.com'\n : `https://${region}-aiplatform.googleapis.com`;\n url = projectId\n ? `https://aiplatform.googleapis.com/v1/projects/${projectId}/locations/${region}/publishers/google/models/${model}:${responseType}?key=${keyParam}${stream ? '&alt=sse' : ''}`\n : `${baseUrl}/v1/publishers/google/models/${model}:${responseType}?key=${keyParam}${stream ? '&alt=sse' : ''}`;\n } else if (authType === 'full') {\n // For Full mode (service account authentication), use project-specific URL\n // Get project ID from Service Account JSON\n const serviceAccountJson = readSecret(request.user.directories, SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT);\n if (!serviceAccountJson) {\n console.warn('Vertex AI Service Account JSON is missing.');\n return response.status(400).send({ error: true });\n }\n\n let projectId;\n try {\n const serviceAccount = JSON.parse(serviceAccountJson);\n projectId = getProjectIdFromServiceAccount(serviceAccount);\n } catch (error) {\n console.error('Failed to extract project ID from Service Account JSON:', error);\n return response.status(400).send({ error: true });\n }\n const region = request.body.vertexai_region || 'us-central1';\n // Handle global region differently - no region prefix in hostname\n if (region === 'global') {\n url = `https://aiplatform.googleapis.com/v1/projects/${projectId}/locations/${region}/publishers/google/models/${model}:${responseType}${stream ? '?alt=sse' : ''}`;\n } else {\n url = `https://${region}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${region}/publishers/google/models/${model}:${responseType}${stream ? '?alt=sse' : ''}`;\n }\n headers['Authorization'] = authHeader;\n } else {\n // For proxy mode, use the original URL with Authorization header\n url = `${apiUrl.toString().replace(/\\/$/, '')}/v1/publishers/google/models/${model}:${responseType}${stream ? '?alt=sse' : ''}`;\n headers['Authorization'] = authHeader;\n }\n } else {\n url = `${apiUrl.toString().replace(/\\/$/, '')}/${apiVersion}/models/${model}:${responseType}?key=${apiKey}${stream ? '&alt=sse' : ''}`;\n }\n\n const generateResponse = await fetch(url, {\n body: JSON.stringify(body),\n method: 'POST',\n headers: headers,\n signal: controller.signal,\n });\n\n if (stream) {\n try {\n // Pipe remote SSE stream to Express response\n forwardFetchResponse(generateResponse, response);\n } catch (error) {\n console.error('Error forwarding streaming response:', error);\n if (!response.headersSent) {\n return response.status(500).send({ error: true });\n }\n }\n } else {\n if (!generateResponse.ok) {\n const errorText = await generateResponse.text();\n console.warn(`${apiName} API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);\n const errorJson = tryParse(errorText) ?? { error: true };\n return response.status(500).send(errorJson);\n }\n\n /** @type {any} */\n const generateResponseJson = await generateResponse.json();\n\n const candidates = generateResponseJson?.candidates;\n if (!candidates || candidates.length === 0) {\n let message = `${apiName} API returned no candidate`;\n console.warn(message, generateResponseJson);\n if (generateResponseJson?.promptFeedback?.blockReason) {\n message += `\\nPrompt was blocked due to : ${generateResponseJson.promptFeedback.blockReason}`;\n }\n return response.send({ error: { message } });\n }\n\n const responseContent = candidates[0].content ?? candidates[0].output;\n const functionCall = (candidates?.[0]?.content?.parts ?? []).some(part => part.functionCall);\n const inlineData = (candidates?.[0]?.content?.parts ?? []).some(part => part.inlineData);\n console.debug(`${apiName} response:`, util.inspect(generateResponseJson, { depth: 5, colors: true }));\n\n const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.filter(part => !part.thought)?.map(part => part.text)?.join('\\n\\n');\n if (!responseText && !functionCall && !inlineData) {\n let message = `${apiName} Candidate text empty`;\n console.warn(message, generateResponseJson);\n return response.send({ error: { message } });\n }\n\n // Wrap it back to OAI format (responseContent includes thought signatures in parts array)\n const reply = { choices: [{ 'message': { 'content': responseText } }], responseContent };\n return response.send(reply);\n }\n } catch (error) {\n console.error(`Error communicating with ${apiName} API:`, error);\n if (!response.headersSent) {\n return response.status(500).send({ error: true });\n }\n }\n}\n\n/**\n * Sends a request to AI21 API.\n * @param {express.Request} request Express request\n * @param {express.Response} response Express response\n */\nasync function sendAI21Request(request, response) {\n if (!request.body) return response.sendStatus(400);\n\n const apiKey = readSecret(request.user.directories, SECRET_KEYS.AI21);\n if (!apiKey) {\n console.warn('AI21 API key is missing.');\n return response.status(400).send({ error: true });\n }\n\n const bodyParams = {};\n const controller = new AbortController();\n request.socket.removeAllListeners('close');\n request.socket.on('close', function () {\n controller.abort();\n });\n // Hack to support JSON schema\n if (request.body.json_schema) {\n bodyParams.response_format = {\n type: 'json_object',\n };\n const message = {\n role: 'user',\n content: `JSON schema for the response:\\n${JSON.stringify(request.body.json_schema.value, null, 4)}`,\n };\n request.body.messages.push(message);\n }\n const convertedPrompt = convertAI21Messages(request.body.messages, getPromptNames(request));\n const body = {\n messages: convertedPrompt,\n model: request.body.model,\n max_tokens: request.body.max_tokens,\n temperature: request.body.temperature,\n top_p: request.body.top_p,\n stop: request.body.stop,\n stream: request.body.stream,\n tools: request.body.tools,\n ...bodyParams,\n };\n const options = {\n method: 'POST',\n headers: {\n accept: 'application/json',\n 'content-type': 'application/json',\n Authorization: `Bearer ${apiKey}`,\n },\n body: JSON.stringify(body),\n signal: controller.signal,\n };\n\n console.debug('AI21 request:', body);\n\n try {\n const generateResponse = await fetch(API_AI21 + '/chat/completions', options);\n if (request.body.stream) {\n forwardFetchResponse(generateResponse, response);\n } else {\n if (!generateResponse.ok) {\n const errorText = await generateResponse.text();\n console.warn(`AI21 API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);\n const errorJson = tryParse(errorText) ?? { error: true };\n return response.status(500).send(errorJson);\n }\n const generateResponseJson = await generateResponse.json();\n console.debug('AI21 response:', generateResponseJson);\n return response.send(generateResponseJson);\n }\n } catch (error) {\n console.error('Error communicating with AI21 API: ', error);\n if (!response.headersSent) {\n response.send({ error: true });\n } else {\n response.end();\n }\n }\n}\n\n/**\n * Sends a request to MistralAI API.\n * @param {express.Request} request Express request\n * @param {express.Response} response Express response\n */\nasync function sendMistralAIRequest(request, response) {\n const apiUrl = new URL(request.body.reverse_proxy || API_MISTRAL).toString();\n const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MISTRALAI);\n\n if (!apiKey) {\n console.warn('MistralAI API key is missing.');\n return response.status(400).send({ error: true });\n }\n\n try {\n const messages = convertMistralMessages(request.body.messages, getPromptNames(request));\n const controller = new AbortController();\n request.socket.removeAllListeners('close');\n request.socket.on('close', function () {\n controller.abort();\n });\n\n const requestBody = {\n 'model': request.body.model,\n 'messages': messages,\n 'temperature': request.body.temperature,\n 'top_p': request.body.top_p,\n 'frequency_penalty': request.body.frequency_penalty,\n 'presence_penalty': request.body.presence_penalty,\n 'max_tokens': request.body.max_tokens,\n 'stream': request.body.stream,\n 'safe_prompt': request.body.safe_prompt,\n 'random_seed': request.body.seed === -1 ? undefined : request.body.seed,\n 'stop': Array.isArray(request.body.stop) && request.body.stop.length > 0 ? request.body.stop : undefined,\n };\n\n if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {\n requestBody['tools'] = request.body.tools;\n requestBody['tool_choice'] = request.body.tool_choice;\n }\n\n if (request.body.json_schema) {\n requestBody['response_format'] = {\n type: 'json_schema',\n json_schema: {\n name: request.body.json_schema.name,\n description: request.body.json_schema.description,\n schema: request.body.json_schema.value,\n strict: request.body.json_schema.strict ?? true,\n },\n };\n }\n\n const config = {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer ' + apiKey,\n },\n body: JSON.stringify(requestBody),\n signal: controller.signal,\n timeout: 0,\n };\n\n console.debug('MisralAI request:', requestBody);\n\n const generateResponse = await fetch(apiUrl + '/chat/completions', config);\n if (request.body.stream) {\n forwardFetchResponse(generateResponse, response);\n } else {\n if (!generateResponse.ok) {\n const errorText = await generateResponse.text();\n console.warn(`MistralAI API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);\n const errorJson = tryParse(errorText) ?? { error: true };\n return response.status(500).send(errorJson);\n }\n const generateResponseJson = await generateResponse.json();\n console.debug('MistralAI response:', generateResponseJson);\n return response.send(generateResponseJson);\n }\n } catch (error) {\n console.error('Error communicating with MistralAI API: ', error);\n if (!response.headersSent) {\n response.send({ error: true });\n } else {\n response.end();\n }\n }\n}\n\n/**\n * Sends a request to Cohere API.\n * @param {express.Request} request Express request\n * @param {express.Response} response Express response\n */\nasync function sendCohereRequest(request, response) {\n const apiKey = readSecret(request.user.directories, SECRET_KEYS.COHERE);\n const controller = new AbortController();\n request.socket.removeAllListeners('close');\n request.socket.on('close', function () {\n controller.abort();\n });\n\n if (!apiKey) {\n console.warn('Cohere API key is missing.');\n return response.status(400).send({ error: true });\n }\n\n try {\n const convertedHistory = convertCohereMessages(request.body.messages, getPromptNames(request));\n const tools = [];\n\n if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {\n tools.push(...request.body.tools);\n tools.forEach(tool => {\n if (tool?.function?.parameters?.$schema) {\n delete tool.function.parameters.$schema;\n }\n });\n }\n\n // https://docs.cohere.com/reference/chat\n const requestBody = {\n stream: Boolean(request.body.stream),\n model: request.body.model,\n messages: convertedHistory.chatHistory,\n temperature: request.body.temperature,\n max_tokens: request.body.max_tokens,\n k: request.body.top_k,\n p: request.body.top_p,\n seed: request.body.seed,\n stop_sequences: request.body.stop,\n frequency_penalty: request.body.frequency_penalty,\n presence_penalty: request.body.presence_penalty,\n documents: [],\n tools: tools,\n };\n\n const canDoSafetyMode = String(request.body.model).endsWith('08-2024');\n if (canDoSafetyMode) {\n requestBody.safety_mode = 'OFF';\n }\n\n if (request.body.json_schema) {\n requestBody.response_format = {\n type: 'json_schema',\n schema: request.body.json_schema.value,\n };\n }\n\n console.debug('Cohere request:', requestBody);\n\n const config = {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer ' + apiKey,\n },\n body: JSON.stringify(requestBody),\n signal: controller.signal,\n timeout: 0,\n };\n\n const apiUrl = API_COHERE_V2 + '/chat';\n\n if (request.body.stream) {\n const stream = await fetch(apiUrl, config);\n forwardFetchResponse(stream, response);\n } else {\n const generateResponse = await fetch(apiUrl, config);\n if (!generateResponse.ok) {\n const errorText = await generateResponse.text();\n console.warn(`Cohere API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);\n const errorJson = tryParse(errorText) ?? { error: true };\n return response.status(500).send(errorJson);\n }\n const generateResponseJson = await generateResponse.json();\n console.debug('Cohere response:', generateResponseJson);\n return response.send(generateResponseJson);\n }\n } catch (error) {\n console.error('Error communicating with Cohere API: ', error);\n if (!response.headersSent) {\n response.send({ error: true });\n } else {\n response.end();\n }\n }\n}\n\n/**\n * Sends a request to DeepSeek API.\n * @param {express.Request} request Express request\n * @param {express.Response} response Express response\n */\nasync function sendDeepSeekRequest(request, response) {\n const apiUrl = new URL(request.body.reverse_proxy || API_DEEPSEEK).toString();\n const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK);\n\n if (!apiKey && !request.body.reverse_proxy) {\n console.warn('DeepSeek API key is missing.');\n return response.status(400).send({ error: true });\n }\n\n const controller = new AbortController();\n request.socket.removeAllListeners('close');\n request.socket.on('close', function () {\n controller.abort();\n });\n\n try {\n let bodyParams = {};\n\n if (request.body.logprobs > 0) {\n bodyParams['top_logprobs'] = request.body.logprobs;\n bodyParams['logprobs'] = true;\n }\n\n if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {\n bodyParams['tools'] = request.body.tools;\n bodyParams['tool_choice'] = request.body.tool_choice;\n\n // DeepSeek doesn't permit empty required arrays\n bodyParams.tools.forEach(tool => {\n const required = tool?.function?.parameters?.required;\n if (Array.isArray(required) && required.length === 0) {\n delete tool.function.parameters.required;\n }\n });\n }\n\n // Hack to support JSON schema\n if (request.body.json_schema) {\n bodyParams.response_format = {\n type: 'json_object',\n };\n const message = {\n role: 'user',\n content: `JSON schema for the response:\\n${JSON.stringify(request.body.json_schema.value, null, 4)}`,\n };\n request.body.messages.push(message);\n }\n\n const processedMessages = addAssistantPrefix(postProcessPrompt(request.body.messages, PROMPT_PROCESSING_TYPE.SEMI_TOOLS, getPromptNames(request)), bodyParams.tools, 'prefix');\n\n if (/-reasoner/.test(request.body.model)) {\n addReasoningContentToToolCalls(processedMessages);\n }\n\n const requestBody = {\n 'messages': processedMessages,\n 'model': request.body.model,\n 'temperature': request.body.temperature,\n 'max_tokens': request.body.max_tokens,\n 'stream': request.body.stream,\n 'presence_penalty': request.body.presence_penalty,\n 'frequency_penalty': request.body.frequency_penalty,\n 'top_p': request.body.top_p,\n 'stop': request.body.stop,\n 'seed': request.body.seed,\n ...bodyParams,\n };\n\n const config = {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer ' + apiKey,\n },\n body: JSON.stringify(requestBody),\n signal: controller.signal,\n };\n\n console.debug('DeepSeek request:', requestBody);\n\n const generateResponse = await fetch(apiUrl + '/chat/completions', config);\n\n if (request.body.stream) {\n forwardFetchResponse(generateResponse, response);\n } else {\n if (!generateResponse.ok) {\n const errorText = await generateResponse.text();\n console.warn(`DeepSeek API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);\n const errorJson = tryParse(errorText) ?? { error: true };\n return response.status(500).send(errorJson);\n }\n const generateResponseJson = await generateResponse.json();\n console.debug('DeepSeek response:', generateResponseJson);\n return response.send(generateResponseJson);\n }\n } catch (error) {\n console.error('Error communicating with DeepSeek API: ', error);\n if (!response.headersSent) {\n response.send({ error: true });\n } else {\n response.end();\n }\n }\n}\n\n/**\n * Sends a request to XAI API.\n * @param {express.Request} request Express request\n * @param {express.Response} response Express response\n */\nasync function sendXaiRequest(request, response) {\n const apiUrl = new URL(request.body.reverse_proxy || API_XAI).toString();\n const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.XAI);\n\n if (!apiKey && !request.body.reverse_proxy) {\n console.warn('xAI API key is missing.');\n return response.status(400).send({ error: true });\n }\n\n const controller = new AbortController();\n request.socket.removeAllListeners('close');\n request.socket.on('close', function () {\n controller.abort();\n });\n\n try {\n let bodyParams = {};\n\n if (request.body.logprobs > 0) {\n bodyParams['top_logprobs'] = request.body.logprobs;\n bodyParams['logprobs'] = true;\n }\n\n if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {\n bodyParams['tools'] = request.body.tools;\n bodyParams['tool_choice'] = request.body.tool_choice;\n }\n\n if (Array.isArray(request.body.stop) && request.body.stop.length > 0) {\n bodyParams['stop'] = request.body.stop;\n }\n\n if (request.body.reasoning_effort) {\n bodyParams['reasoning_effort'] = request.body.reasoning_effort === 'high' ? 'high' : 'low';\n }\n\n if (request.body.enable_web_search) {\n bodyParams['search_parameters'] = {\n mode: 'on',\n sources: [\n { type: 'web', safe_search: false },\n { type: 'news', safe_search: false },\n { type: 'x' },\n ],\n };\n }\n\n if (request.body.json_schema) {\n bodyParams['response_format'] = {\n type: 'json_schema',\n json_schema: {\n name: request.body.json_schema.name,\n strict: request.body.json_schema.strict ?? true,\n schema: request.body.json_schema.value,\n },\n };\n }\n\n const processedMessages = request.body.messages = convertXAIMessages(request.body.messages, getPromptNames(request));\n\n const requestBody = {\n 'messages': processedMessages,\n 'model': request.body.model,\n 'temperature': request.body.temperature,\n 'max_tokens': request.body.max_tokens,\n 'max_completion_tokens': request.body.max_completion_tokens,\n 'stream': request.body.stream,\n 'presence_penalty': request.body.presence_penalty,\n 'frequency_penalty': request.body.frequency_penalty,\n 'top_p': request.body.top_p,\n 'seed': request.body.seed,\n 'n': request.body.n,\n ...bodyParams,\n };\n\n const config = {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer ' + apiKey,\n },\n body: JSON.stringify(requestBody),\n signal: controller.signal,\n };\n\n console.debug('xAI request:', requestBody);\n\n const generateResponse = await fetch(apiUrl + '/chat/completions', config);\n\n if (request.body.stream) {\n forwardFetchResponse(generateResponse, response);\n } else {\n if (!generateResponse.ok) {\n const errorText = await generateResponse.text();\n console.warn(`xAI API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);\n const errorJson = tryParse(errorText) ?? { error: true };\n return response.status(500).send(errorJson);\n }\n const generateResponseJson = await generateResponse.json();\n console.debug('xAI response:', generateResponseJson);\n return response.send(generateResponseJson);\n }\n } catch (error) {\n console.error('Error communicating with xAI API: ', error);\n if (!response.headersSent) {\n response.send({ error: true });\n } else {\n response.end();\n }\n }\n}\n\n/**\n * Sends a request to AI/ML API.\n * @param {express.Request} request Express request\n * @param {express.Response} response Express response\n */\nasync function sendAimlapiRequest(request, response) {\n const apiUrl = API_AIMLAPI;\n const apiKey = readSecret(request.user.directories, SECRET_KEYS.AIMLAPI);\n\n if (!apiKey) {\n console.warn('AI/ML API key is missing.');\n return response.status(400).send({ error: true });\n }\n\n const controller = new AbortController();\n request.socket.removeAllListeners('close');\n request.socket.on('close', function () {\n controller.abort();\n });\n\n try {\n let bodyParams = {};\n\n if (request.body.logprobs > 0) {\n bodyParams['top_logprobs'] = request.body.logprobs;\n bodyParams['logprobs'] = true;\n }\n\n if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {\n bodyParams['tools'] = request.body.tools;\n bodyParams['tool_choice'] = request.body.tool_choice;\n }\n\n if (Array.isArray(request.body.stop) && request.body.stop.length > 0) {\n bodyParams['stop'] = request.body.stop;\n }\n\n if (request.body.reasoning_effort) {\n bodyParams['reasoning_effort'] = request.body.reasoning_effort;\n }\n\n if (request.body.json_schema) {\n bodyParams['response_format'] = {\n type: 'json_schema',\n json_schema: {\n name: request.body.json_schema.name,\n description: request.body.json_schema.description,\n schema: request.body.json_schema.value,\n strict: request.body.json_schema.strict ?? true,\n },\n };\n }\n\n const requestBody = {\n 'messages': request.body.messages,\n 'model': request.body.model,\n 'temperature': request.body.temperature,\n 'max_tokens': request.body.max_tokens,\n 'stream': request.body.stream,\n 'presence_penalty': request.body.presence_penalty,\n 'frequency_penalty': request.body.frequency_penalty,\n 'top_p': request.body.top_p,\n 'seed': request.body.seed,\n 'n': request.body.n,\n ...bodyParams,\n };\n\n const config = {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer ' + apiKey,\n ...AIMLAPI_HEADERS,\n },\n body: JSON.stringify(requestBody),\n signal: controller.signal,\n };\n\n console.debug('AI/ML API request:', requestBody);\n\n const generateResponse = await fetch(apiUrl + '/chat/completions', config);\n\n if (request.body.stream) {\n forwardFetchResponse(generateResponse, response);\n } else {\n if (!generateResponse.ok) {\n const errorText = await generateResponse.text();\n console.warn(`AI/ML API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);\n const errorJson = tryParse(errorText) ?? { error: true };\n return response.status(500).send(errorJson);\n }\n const generateResponseJson = await generateResponse.json();\n console.debug('AI/ML API response:', generateResponseJson);\n return response.send(generateResponseJson);\n }\n } catch (error) {\n console.error('Error communicating with AI/ML API: ', error);\n if (!response.headersSent) {\n response.send({ error: true });\n } else {\n response.end();\n }\n }\n}\n\n/**\n * Sends a request to Electron Hub.\n * @param {express.Request} request Express request\n * @param {express.Response} response Express response\n */\nasync function sendElectronHubRequest(request, response) {\n const apiUrl = API_ELECTRONHUB;\n const apiKey = readSecret(request.user.directories, SECRET_KEYS.ELECTRONHUB);\n\n if (!apiKey) {\n console.warn('Electron Hub key is missing.');\n return response.status(400).send({ error: true });\n }\n\n const controller = new AbortController();\n request.socket.removeAllListeners('close');\n request.socket.on('close', function () {\n controller.abort();\n });\n\n try {\n let bodyParams = {};\n\n if (request.body.enable_web_search) {\n bodyParams['web_search'] = true;\n }\n\n if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {\n bodyParams['tools'] = request.body.tools;\n bodyParams['tool_choice'] = request.body.tool_choice;\n }\n\n if (request.body.reasoning_effort) {\n bodyParams['reasoning_effort'] = request.body.reasoning_effort;\n }\n\n if (request.body.json_schema) {\n bodyParams['response_format'] = {\n type: 'json_schema',\n json_schema: {\n name: request.body.json_schema.name,\n description: request.body.json_schema.description,\n schema: request.body.json_schema.value,\n strict: request.body.json_schema.strict ?? true,\n },\n };\n }\n\n const isClaude = /^claude-/.test(request.body.model);\n\n if (Array.isArray(request.body.messages) && isClaude) {\n if (enableSystemPromptCache) {\n cachingSystemPromptForOpenRouter(request.body.messages, cacheTTL);\n }\n\n if (cachingAtDepth !== -1) {\n cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth, cacheTTL);\n }\n }\n\n const requestBody = {\n 'messages': request.body.messages,\n 'model': request.body.model,\n 'temperature': request.body.temperature,\n 'max_tokens': request.body.max_tokens,\n 'stream': request.body.stream,\n 'presence_penalty': request.body.presence_penalty,\n 'frequency_penalty': request.body.frequency_penalty,\n 'top_p': request.body.top_p,\n 'top_k': request.body.top_k,\n 'logit_bias': request.body.logit_bias,\n 'seed': request.body.seed,\n ...bodyParams,\n };\n\n const config = {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer ' + apiKey,\n },\n body: JSON.stringify(requestBody),\n signal: controller.signal,\n };\n\n console.debug('Electron Hub request:', requestBody);\n\n const generateResponse = await fetch(apiUrl + '/chat/completions', config);\n\n if (request.body.stream) {\n forwardFetchResponse(generateResponse, response);\n } else {\n if (!generateResponse.ok) {\n const errorText = await generateResponse.text();\n console.warn('Electron Hub returned error: ', errorText);\n const errorJson = tryParse(errorText) ?? { error: true };\n return response.status(500).send(errorJson);\n }\n const generateResponseJson = await generateResponse.json();\n console.debug('Electron Hub response:', generateResponseJson);\n return response.send(generateResponseJson);\n }\n }\n catch (error) {\n console.error('Error communicating with Electron Hub: ', error);\n if (!response.headersSent) {\n response.send({ error: true });\n } else {\n response.end();\n }\n }\n}\n\n/**\n * Sends a request to Chutes.\n * @param {express.Request} request Express request\n * @param {express.Response} response Express response\n */\nasync function sendChutesRequest(request, response) {\n const apiUrl = API_CHUTES;\n const apiKey = readSecret(request.user.directories, SECRET_KEYS.CHUTES);\n\n if (!apiKey) {\n console.warn('Chutes key is missing.');\n return response.status(400).send({ error: true });\n }\n\n const controller = new AbortController();\n request.socket.removeAllListeners('close');\n request.socket.on('close', function () {\n controller.abort();\n });\n\n try {\n let bodyParams = {};\n\n if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {\n bodyParams['tools'] = request.body.tools;\n bodyParams['tool_choice'] = request.body.tool_choice;\n }\n\n if (request.body.logprobs > 0) {\n bodyParams['top_logprobs'] = request.body.logprobs;\n bodyParams['logprobs'] = true;\n }\n\n if (request.body.json_schema) {\n bodyParams['response_format'] = {\n type: 'json_schema',\n json_schema: {\n name: request.body.json_schema.name,\n description: request.body.json_schema.description,\n schema: request.body.json_schema.value,\n strict: request.body.json_schema.strict ?? true,\n },\n };\n }\n\n const requestBody = {\n 'messages': request.body.messages,\n 'model': request.body.model,\n 'temperature': request.body.temperature,\n 'max_tokens': request.body.max_tokens,\n 'stream': request.body.stream,\n 'presence_penalty': request.body.presence_penalty,\n 'frequency_penalty': request.body.frequency_penalty,\n 'repetition_penalty': request.body.repetition_penalty,\n 'min_p': request.body.min_p,\n 'top_p': request.body.top_p,\n 'top_k': request.body.top_k,\n 'seed': request.body.seed,\n 'stop': request.body.stop,\n 'reasoning_effort': request.body.reasoning_effort,\n 'logit_bias': request.body.logit_bias,\n ...bodyParams,\n };\n\n const config = {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer ' + apiKey,\n },\n body: JSON.stringify(requestBody),\n signal: controller.signal,\n };\n\n console.debug('Chutes request:', requestBody);\n\n const generateResponse = await fetch(apiUrl + '/chat/completions', config);\n\n if (request.body.stream) {\n forwardFetchResponse(generateResponse, response);\n } else {\n if (!generateResponse.ok) {\n const errorText = await generateResponse.text();\n console.warn('Chutes returned error: ', errorText);\n const errorJson = tryParse(errorText) ?? { error: true };\n return response.status(500).send(errorJson);\n }\n const generateResponseJson = await generateResponse.json();\n console.debug('Chutes response:', generateResponseJson);\n return response.send(generateResponseJson);\n }\n }\n catch (error) {\n console.error('Error communicating with Chutes: ', error);\n if (!response.headersSent) {\n response.send({ error: true });\n } else {\n response.end();\n }\n }\n}\n\n/**\n * Sends a chat completion request to Azure OpenAI.\n * @param {express.Request} request Express request object (contains request.body with all generate_data)\n * @param {express.Response} response Express response object\n */\nasync function sendAzureOpenAIRequest(request, response) {\n // 1. GATHER & VALIDATE SETTINGS\n const { azure_base_url, azure_deployment_name, azure_api_version } = request.body;\n const apiKey = readSecret(request.user.directories, SECRET_KEYS.AZURE_OPENAI);\n if (!azure_base_url || !azure_deployment_name || !azure_api_version || !apiKey) {\n return response.status(400).send({\n error: {\n message: 'Azure OpenAI configuration is incomplete. Please provide Base URL, Deployment Name, API Version, and API Key in the connection settings.',\n },\n });\n }\n\n // 2. PREPARE THE REQUEST\n const url = new URL(`/openai/deployments/${azure_deployment_name}/chat/completions`, azure_base_url);\n url.searchParams.set('api-version', azure_api_version);\n const endpointUrl = url.toString();\n\n // Create the base payload with all standard parameters\n const apiRequestBody = /** @type {any} */ ({});\n for (const key of AZURE_OPENAI_KEYS) {\n if (Object.hasOwn(request.body, key)) {\n apiRequestBody[key] = request.body[key];\n }\n }\n\n // Handle Structured Output (JSON Mode) by translating the custom `json_schema` object.\n if (request.body.json_schema) {\n apiRequestBody['response_format'] = {\n type: 'json_schema',\n json_schema: {\n name: request.body.json_schema.name,\n strict: request.body.json_schema.strict ?? true,\n schema: request.body.json_schema.value,\n },\n };\n }\n\n // Adjust logprobs for Azure OpenAI, which follows the OpenAI Chat Completions API spec.\n if (typeof apiRequestBody.logprobs === 'number' && apiRequestBody.logprobs > 0) {\n apiRequestBody.top_logprobs = apiRequestBody.logprobs;\n apiRequestBody.logprobs = true;\n }\n\n // Do not send reasoning effort to models which do not support it\n apiRequestBody['reasoning_effort'] = OPENAI_REASONING_EFFORT_MODELS.includes(request.body.model)\n ? OPENAI_REASONING_EFFORT_MAP[request.body.reasoning_effort] ?? request.body.reasoning_effort\n : undefined;\n\n const controller = new AbortController();\n request.socket.removeAllListeners('close');\n request.socket.on('close', () => controller.abort());\n\n const config = {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'api-key': apiKey,\n },\n body: JSON.stringify(apiRequestBody),\n signal: controller.signal,\n };\n\n console.info(`Sending request to Azure OpenAI: ${endpointUrl}`);\n console.debug('Azure OpenAI Request Body:', apiRequestBody);\n try {\n const fetchResponse = await fetch(endpointUrl, config);\n\n if (request.body.stream) {\n return forwardFetchResponse(fetchResponse, response);\n }\n\n if (fetchResponse.ok) {\n /** @type {any} */\n const json = await fetchResponse.json();\n console.debug('Azure OpenAI response:', json);\n return response.send(json);\n }\n\n const text = await fetchResponse.text();\n const data = tryParse(text) || { error: { message: fetchResponse.statusText || 'Unknown error occurred' } };\n return response.status(500).send(data);\n } catch (error) {\n const message = error.name === 'AbortError'\n ? 'Request was aborted by the client.'\n : (error.message || 'An unknown network error occurred.');\n return response.status(500).send({ error: { message, ...error } });\n }\n}\n\nexport const router = express.Router();\n\nrouter.post('/status', async function (request, statusResponse) {\n try {\n if (!request.body) return statusResponse.sendStatus(400);\n\n let apiUrl = '';\n let apiKey = '';\n let headers = {};\n let queryParams = {};\n\n if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.OPENAI) {\n apiUrl = new URL(request.body.reverse_proxy || API_OPENAI).toString();\n apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.OPENAI);\n headers = {};\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.OPENROUTER) {\n apiUrl = 'https://openrouter.ai/api/v1';\n apiKey = readSecret(request.user.directories, SECRET_KEYS.OPENROUTER);\n // OpenRouter needs to pass the Referer and X-Title: https://openrouter.ai/docs#requests\n headers = { ...OPENROUTER_HEADERS };\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.MISTRALAI) {\n apiUrl = new URL(request.body.reverse_proxy || API_MISTRAL).toString();\n apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MISTRALAI);\n headers = {};\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.CUSTOM) {\n apiUrl = request.body.custom_url;\n apiKey = readSecret(request.user.directories, SECRET_KEYS.CUSTOM);\n headers = {};\n mergeObjectWithYaml(headers, request.body.custom_include_headers);\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.COHERE) {\n apiUrl = API_COHERE_V1;\n apiKey = readSecret(request.user.directories, SECRET_KEYS.COHERE);\n headers = {};\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.CHUTES) {\n apiUrl = API_CHUTES;\n apiKey = readSecret(request.user.directories, SECRET_KEYS.CHUTES);\n headers = {};\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.ELECTRONHUB) {\n apiUrl = API_ELECTRONHUB;\n apiKey = readSecret(request.user.directories, SECRET_KEYS.ELECTRONHUB);\n headers = {};\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.NANOGPT) {\n apiUrl = API_NANOGPT;\n apiKey = readSecret(request.user.directories, SECRET_KEYS.NANOGPT);\n headers = {};\n queryParams = { detailed: true };\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.DEEPSEEK) {\n apiUrl = new URL(request.body.reverse_proxy || API_DEEPSEEK.replace('/beta', '')).toString();\n apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.DEEPSEEK);\n headers = {};\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.XAI) {\n apiUrl = new URL(request.body.reverse_proxy || API_XAI).toString();\n apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.XAI);\n headers = {};\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.AIMLAPI) {\n apiUrl = API_AIMLAPI;\n apiKey = readSecret(request.user.directories, SECRET_KEYS.AIMLAPI);\n headers = { ...AIMLAPI_HEADERS };\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.POLLINATIONS) {\n apiUrl = 'https://text.pollinations.ai';\n apiKey = 'NONE';\n headers = {};\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.GROQ) {\n apiUrl = API_GROQ;\n apiKey = readSecret(request.user.directories, SECRET_KEYS.GROQ);\n headers = {};\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.COMETAPI) {\n apiUrl = API_COMETAPI;\n apiKey = readSecret(request.user.directories, SECRET_KEYS.COMETAPI);\n headers = {};\n throw new Error('This provider is temporarily disabled.');\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.MOONSHOT) {\n apiUrl = API_MOONSHOT;\n apiKey = readSecret(request.user.directories, SECRET_KEYS.MOONSHOT);\n headers = {};\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.FIREWORKS) {\n apiUrl = API_FIREWORKS;\n apiKey = readSecret(request.user.directories, SECRET_KEYS.FIREWORKS);\n headers = {};\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.MAKERSUITE) {\n apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MAKERSUITE);\n apiUrl = trimTrailingSlash(request.body.reverse_proxy || API_MAKERSUITE);\n const apiVersion = getConfigValue('gemini.apiVersion', 'v1beta');\n const modelsUrl = !apiKey && request.body.reverse_proxy\n ? `${apiUrl}/${apiVersion}/models`\n : `${apiUrl}/${apiVersion}/models?key=${apiKey}`;\n\n if (!apiKey && !request.body.reverse_proxy) {\n console.warn('Google AI Studio API key is missing.');\n return statusResponse.status(400).send({ error: true });\n }\n\n try {\n const response = await fetch(modelsUrl);\n\n if (response.ok) {\n /** @type {any} */\n const data = await response.json();\n // Transform Google AI Studio models to OpenAI format\n const models = data.models\n ?.filter(model => model.supportedGenerationMethods?.includes('generateContent'))\n ?.map(model => ({\n id: model.name.replace('models/', ''),\n })) || [];\n\n console.info('Available Google AI Studio models:', models.map(m => m.id));\n return statusResponse.send({ data: models });\n } else {\n console.warn('Google AI Studio models endpoint failed:', response.status, response.statusText);\n return statusResponse.send({ error: true, bypass: true, data: { data: [] } });\n }\n } catch (error) {\n console.error('Error fetching Google AI Studio models:', error);\n return statusResponse.send({ error: true, bypass: true, data: { data: [] } });\n }\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.AZURE_OPENAI) {\n const { azure_base_url, azure_deployment_name, azure_api_version } = request.body;\n const apiKey = readSecret(request.user.directories, SECRET_KEYS.AZURE_OPENAI);\n\n // 1) Validate configuration from the frontend\n if (!apiKey || !azure_base_url || !azure_deployment_name || !azure_api_version) {\n console.warn('Azure OpenAI status check failed: missing config from frontend.');\n return statusResponse.status(400).send({ error: true, message: 'Azure configuration is incomplete.' });\n }\n // 2) Build URLs using the URL API for consistency and robustness.\n const modelsUrl = new URL('/openai/models', azure_base_url);\n modelsUrl.searchParams.set('api-version', azure_api_version);\n\n const chatUrl = new URL(`/openai/deployments/${azure_deployment_name}/chat/completions`, azure_base_url);\n chatUrl.searchParams.set('api-version', azure_api_version);\n\n // Map common status codes to user-friendly error messages\n const azureStatusErrorMap = {\n 400: 'API version may be invalid for this resource.',\n 401: 'Invalid API key or insufficient permissions.',\n 403: 'Invalid API key or insufficient permissions.',\n 404: 'Endpoint URL appears incorrect (404).',\n };\n\n try {\n // ---- A) GET /models: fast sanity check for endpoint + api key + api version ----\n const apiConfigTest = await fetch(modelsUrl, {\n method: 'GET',\n headers: { 'api-key': apiKey, 'Accept': 'application/json' },\n });\n\n if (!apiConfigTest.ok) {\n let errText = '';\n try { errText = await apiConfigTest.text(); } catch { /* response body may be empty */ }\n\n console.warn('Azure OpenAI GET /models failed:', apiConfigTest.status, apiConfigTest.statusText, errText || '');\n\n const defaultMessage = `Azure Models endpoint error: ${apiConfigTest.statusText}`;\n const message = azureStatusErrorMap[apiConfigTest.status] ?? defaultMessage;\n return statusResponse.status(apiConfigTest.status).send({ error: true, message });\n }\n\n // ---- B) POST /chat/completions: verify deployment + read underlying model ID ----\n // Small, deterministic probe to minimize cost/latency\n const modelPayload = {\n messages: [{ role: 'user', content: 'Say word Hi' }],\n stream: false,\n max_completion_tokens: 5,\n };\n\n const modelRequest = await fetch(chatUrl, {\n method: 'POST',\n headers: { 'api-key': apiKey, 'Content-Type': 'application/json', 'Accept': 'application/json' },\n body: JSON.stringify(modelPayload),\n });\n\n let modelResponse;\n try {\n modelResponse = await modelRequest.json();\n } catch {\n modelResponse = { raw: 'Failed to parse JSON response from chat completions probe.' };\n }\n\n const modelId = /** @type {any} */ (modelResponse)?.model;\n if (!modelId) {\n console.warn('Azure status check succeeded but could not find a model ID in the response.');\n console.debug('Azure Response Body:', modelResponse);\n // Keep a benign success to avoid UX disruption in the UI\n return statusResponse.send({ data: [] });\n }\n\n console.info(color.green('Azure OpenAI connection successful. Detected model:'), modelId);\n // Consistent response format: always an array of { id }\n return statusResponse.send({ data: [{ id: modelId }] });\n } catch (error) {\n console.error('Azure OpenAI status check connection error:', error);\n return statusResponse.status(500).send({ error: true, message: 'Failed to connect to the Azure endpoint.' });\n }\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.SILICONFLOW) {\n apiUrl = API_SILICONFLOW;\n apiKey = readSecret(request.user.directories, SECRET_KEYS.SILICONFLOW);\n headers = {};\n } else {\n console.warn('This chat completion source is not supported yet.');\n return statusResponse.status(400).send({ error: true });\n }\n\n if (!apiKey && !request.body.reverse_proxy && request.body.chat_completion_source !== CHAT_COMPLETION_SOURCES.CUSTOM) {\n console.warn('Chat Completion API key is missing.');\n return statusResponse.status(400).send({ error: true });\n }\n\n const modelsUrl = new URL(urlJoin(apiUrl, '/models'));\n Object.keys(queryParams).forEach(key => {\n modelsUrl.searchParams.append(key, queryParams[key]);\n });\n const response = await fetch(modelsUrl, {\n method: 'GET',\n headers: {\n 'Authorization': 'Bearer ' + apiKey,\n ...headers,\n },\n });\n\n if (response.ok) {\n /** @type {any} */\n let data = await response.json();\n\n if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.POLLINATIONS && Array.isArray(data)) {\n data = { data: data.map(model => ({ id: model.name, ...model })) };\n }\n\n if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.CHUTES && Array.isArray(data?.data)) {\n data.data = data.data\n .filter(model => model?.id)\n .map(model => {\n if (model.pricing?.prompt !== undefined && model.pricing?.completion !== undefined) {\n return {\n ...model,\n pricing: {\n ...model.pricing,\n input: model.pricing.prompt,\n output: model.pricing.completion,\n },\n };\n }\n return model;\n });\n }\n\n statusResponse.send(data);\n\n if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.COHERE && Array.isArray(data?.models)) {\n data.data = data.models.map(model => ({ id: model.name, ...model }));\n }\n\n if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.OPENROUTER && Array.isArray(data?.data)) {\n let models = [];\n\n data.data.forEach(model => {\n const context_length = model.context_length;\n const tokens_dollar = Number(1 / (1000 * model.pricing?.prompt));\n const tokens_rounded = (Math.round(tokens_dollar * 1000) / 1000).toFixed(0);\n models[model.id] = {\n tokens_per_dollar: tokens_rounded + 'k',\n context_length: context_length,\n };\n });\n\n console.info('Available OpenRouter models:', models);\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.MISTRALAI) {\n const models = data?.data;\n console.info(models);\n } else {\n const models = data?.data;\n\n if (Array.isArray(models)) {\n const modelIds = models.filter(x => x && typeof x === 'object').map(x => x.id).sort();\n console.info('Available models:', modelIds);\n } else {\n console.warn('Chat Completion endpoint did not return a list of models.');\n }\n }\n }\n else {\n console.error('Chat Completion status check failed. Either Access Token is incorrect or API endpoint is down.');\n statusResponse.send({ error: true, data: { data: [] } });\n }\n } catch (e) {\n console.error(e);\n\n if (!statusResponse.headersSent) {\n statusResponse.send({ error: true });\n } else {\n statusResponse.end();\n }\n }\n});\n\nrouter.post('/bias', async function (request, response) {\n if (!request.body || !Array.isArray(request.body))\n return response.sendStatus(400);\n\n try {\n const result = {};\n const model = getTokenizerModel(String(request.query.model || ''));\n\n // no bias for claude\n if (model == 'claude') {\n return response.send(result);\n }\n\n let encodeFunction;\n\n if (sentencepieceTokenizers.includes(model)) {\n const tokenizer = getSentencepiceTokenizer(model);\n const instance = await tokenizer?.get();\n if (!instance) {\n console.error('Tokenizer not initialized:', model);\n return response.send({});\n }\n encodeFunction = (text) => new Uint32Array(instance.encodeIds(text));\n } else if (webTokenizers.includes(model)) {\n const tokenizer = getWebTokenizer(model);\n const instance = await tokenizer?.get();\n if (!instance) {\n console.warn('Tokenizer not initialized:', model);\n return response.send({});\n }\n encodeFunction = (text) => new Uint32Array(instance.encode(text));\n } else {\n const tokenizer = getTiktokenTokenizer(model);\n encodeFunction = (tokenizer.encode.bind(tokenizer));\n }\n\n for (const entry of request.body) {\n if (!entry || !entry.text) {\n continue;\n }\n\n try {\n const tokens = getEntryTokens(entry.text, encodeFunction);\n\n for (const token of tokens) {\n result[token] = entry.value;\n }\n } catch {\n console.warn('Tokenizer failed to encode:', entry.text);\n }\n }\n\n // not needed for cached tokenizers\n //tokenizer.free();\n return response.send(result);\n\n /**\n * Gets tokenids for a given entry\n * @param {string} text Entry text\n * @param {(string) => Uint32Array} encode Function to encode text to token ids\n * @returns {Uint32Array} Array of token ids\n */\n function getEntryTokens(text, encode) {\n // Get raw token ids from JSON array\n if (text.trim().startsWith('[') && text.trim().endsWith(']')) {\n try {\n const json = JSON.parse(text);\n if (Array.isArray(json) && json.every(x => typeof x === 'number')) {\n return new Uint32Array(json);\n }\n } catch {\n // ignore\n }\n }\n\n // Otherwise, get token ids from tokenizer\n return encode(text);\n }\n } catch (error) {\n console.error(error);\n return response.send({});\n }\n});\n\nrouter.post('/generate', async function (request, response) {\n try {\n if (!request.body) return response.status(400).send({ error: true });\n\n const postProcessingType = request.body.custom_prompt_post_processing;\n if (Array.isArray(request.body.messages) && postProcessingType) {\n console.info('Applying custom prompt post-processing of type', postProcessingType);\n request.body.messages = postProcessPrompt(\n request.body.messages,\n postProcessingType,\n getPromptNames(request));\n }\n\n if (request.body.json_schema?.value) {\n request.body.json_schema.value = flattenSchema(request.body.json_schema.value, request.body.chat_completion_source);\n }\n\n switch (request.body.chat_completion_source) {\n case CHAT_COMPLETION_SOURCES.CLAUDE: return await sendClaudeRequest(request, response);\n case CHAT_COMPLETION_SOURCES.AI21: return await sendAI21Request(request, response);\n case CHAT_COMPLETION_SOURCES.MAKERSUITE: return await sendMakerSuiteRequest(request, response);\n case CHAT_COMPLETION_SOURCES.VERTEXAI: return await sendMakerSuiteRequest(request, response);\n case CHAT_COMPLETION_SOURCES.MISTRALAI: return await sendMistralAIRequest(request, response);\n case CHAT_COMPLETION_SOURCES.COHERE: return await sendCohereRequest(request, response);\n case CHAT_COMPLETION_SOURCES.DEEPSEEK: return await sendDeepSeekRequest(request, response);\n case CHAT_COMPLETION_SOURCES.AIMLAPI: return await sendAimlapiRequest(request, response);\n case CHAT_COMPLETION_SOURCES.XAI: return await sendXaiRequest(request, response);\n case CHAT_COMPLETION_SOURCES.CHUTES: return await sendChutesRequest(request, response);\n case CHAT_COMPLETION_SOURCES.ELECTRONHUB: return await sendElectronHubRequest(request, response);\n case CHAT_COMPLETION_SOURCES.AZURE_OPENAI: return await sendAzureOpenAIRequest(request, response);\n }\n\n let apiUrl;\n let apiKey;\n let headers;\n let bodyParams;\n const isTextCompletion = Boolean(request.body.model && TEXT_COMPLETION_MODELS.includes(request.body.model)) || typeof request.body.messages === 'string';\n\n if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.OPENAI) {\n apiUrl = new URL(request.body.reverse_proxy || API_OPENAI).toString();\n apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.OPENAI);\n headers = {};\n bodyParams = {\n logprobs: request.body.logprobs,\n top_logprobs: undefined,\n };\n\n // Adjust logprobs params for Chat Completions API, which expects { top_logprobs: number; logprobs: boolean; }\n if (!isTextCompletion && bodyParams.logprobs > 0) {\n bodyParams.top_logprobs = bodyParams.logprobs;\n bodyParams.logprobs = true;\n }\n\n if (getConfigValue('openai.randomizeUserId', false, 'boolean')) {\n bodyParams['user'] = uuidv4();\n }\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.OPENROUTER) {\n apiUrl = 'https://openrouter.ai/api/v1';\n apiKey = readSecret(request.user.directories, SECRET_KEYS.OPENROUTER);\n // OpenRouter needs to pass the Referer and X-Title: https://openrouter.ai/docs#requests\n headers = { ...OPENROUTER_HEADERS };\n bodyParams = {\n 'transforms': getOpenRouterTransforms(request),\n 'plugins': getOpenRouterPlugins(request),\n 'include_reasoning': Boolean(request.body.include_reasoning),\n };\n\n if (request.body.min_p !== undefined) {\n bodyParams['min_p'] = request.body.min_p;\n }\n\n if (request.body.top_a !== undefined) {\n bodyParams['top_a'] = request.body.top_a;\n }\n\n if (request.body.repetition_penalty !== undefined) {\n bodyParams['repetition_penalty'] = request.body.repetition_penalty;\n }\n\n if (Array.isArray(request.body.provider) && request.body.provider.length > 0) {\n bodyParams['provider'] = {\n allow_fallbacks: request.body.allow_fallbacks ?? true,\n order: request.body.provider ?? [],\n };\n }\n\n if (request.body.use_fallback) {\n bodyParams['route'] = 'fallback';\n }\n\n if (request.body.reasoning_effort) {\n bodyParams['reasoning'] = { effort: request.body.reasoning_effort };\n }\n\n if (request.body.verbosity) {\n bodyParams['verbosity'] = request.body.verbosity;\n }\n\n if (request.body.json_schema) {\n bodyParams['response_format'] = {\n type: 'json_schema',\n json_schema: {\n name: request.body.json_schema.name,\n strict: request.body.json_schema.strict ?? true,\n schema: request.body.json_schema.value,\n },\n };\n }\n\n const isClaude = /^anthropic\\/claude/.test(request.body.model);\n const isGemini = /google\\/gemini/.test(request.body.model);\n const isCacheableGemini = isGemini && await isOpenRouterModelCacheable(request.body.model);\n const enableGeminiSystemPromptCache = getConfigValue('gemini.enableSystemPromptCache', false, 'boolean');\n\n if (Array.isArray(request.body.messages)) {\n embedOpenRouterMedia(request.body.messages);\n addOpenRouterSignatures(request.body.messages, request.body.model);\n\n if (isClaude) {\n if (enableSystemPromptCache) {\n cachingSystemPromptForOpenRouter(request.body.messages, cacheTTL);\n }\n\n if (cachingAtDepth !== -1) {\n cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth, cacheTTL);\n }\n }\n\n if (isCacheableGemini && enableGeminiSystemPromptCache) {\n cachingSystemPromptForOpenRouter(request.body.messages);\n }\n }\n\n if (isGemini) {\n bodyParams['safety_settings'] = GEMINI_SAFETY;\n }\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.CUSTOM) {\n apiUrl = request.body.custom_url;\n apiKey = readSecret(request.user.directories, SECRET_KEYS.CUSTOM);\n headers = {};\n bodyParams = {\n logprobs: request.body.logprobs,\n top_logprobs: undefined,\n };\n\n // Adjust logprobs params for Chat Completions API, which expects { top_logprobs: number; logprobs: boolean; }\n if (!isTextCompletion && bodyParams.logprobs > 0) {\n bodyParams.top_logprobs = bodyParams.logprobs;\n bodyParams.logprobs = true;\n }\n\n mergeObjectWithYaml(bodyParams, request.body.custom_include_body);\n mergeObjectWithYaml(headers, request.body.custom_include_headers);\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.PERPLEXITY) {\n apiUrl = API_PERPLEXITY;\n apiKey = readSecret(request.user.directories, SECRET_KEYS.PERPLEXITY);\n headers = {};\n bodyParams = {\n reasoning_effort: request.body.reasoning_effort,\n };\n request.body.messages = postProcessPrompt(request.body.messages, PROMPT_PROCESSING_TYPE.STRICT, getPromptNames(request));\n if (request.body.json_schema) {\n bodyParams['response_format'] = {\n type: 'json_schema',\n json_schema: {\n schema: request.body.json_schema.value,\n },\n };\n }\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.GROQ) {\n apiUrl = API_GROQ;\n apiKey = readSecret(request.user.directories, SECRET_KEYS.GROQ);\n headers = {};\n bodyParams = {};\n if (request.body.json_schema) {\n bodyParams['response_format'] = {\n type: 'json_schema',\n json_schema: {\n name: request.body.json_schema.name,\n description: request.body.json_schema.description,\n schema: request.body.json_schema.value,\n strict: request.body.json_schema.strict ?? true,\n },\n };\n }\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.FIREWORKS) {\n apiUrl = API_FIREWORKS;\n apiKey = readSecret(request.user.directories, SECRET_KEYS.FIREWORKS);\n headers = {};\n bodyParams = {};\n if (request.body.json_schema) {\n bodyParams['response_format'] = {\n type: 'json_schema',\n json_schema: {\n name: request.body.json_schema.name,\n description: request.body.json_schema.description,\n schema: request.body.json_schema.value,\n strict: request.body.json_schema.strict ?? true,\n },\n };\n }\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.NANOGPT) {\n apiUrl = API_NANOGPT;\n apiKey = readSecret(request.user.directories, SECRET_KEYS.NANOGPT);\n headers = {};\n bodyParams = {};\n if (request.body.enable_web_search && !/:online$/.test(request.body.model)) {\n request.body.model = `${request.body.model}:online`;\n }\n if (request.body.min_p !== undefined) {\n bodyParams['min_p'] = request.body.min_p;\n }\n if (request.body.top_a !== undefined) {\n bodyParams['top_a'] = request.body.top_a;\n }\n if (request.body.repetition_penalty !== undefined) {\n bodyParams['repetition_penalty'] = request.body.repetition_penalty;\n }\n\n const isClaude = /^claude-/.test(request.body.model);\n if (enableSystemPromptCache && isClaude) {\n bodyParams['cache_control'] = {\n 'enabled': true,\n 'ttl': cacheTTL,\n };\n }\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.POLLINATIONS) {\n apiUrl = API_POLLINATIONS;\n apiKey = 'NONE';\n headers = {\n 'Authorization': '',\n };\n bodyParams = {\n reasoning_effort: request.body.reasoning_effort,\n private: true,\n referrer: 'sillytavern',\n seed: request.body.seed ?? Math.floor(Math.random() * 99999999),\n };\n if (request.body.json_schema) {\n setJsonObjectFormat(bodyParams, request.body.messages, request.body.json_schema);\n }\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.MOONSHOT) {\n apiUrl = API_MOONSHOT;\n apiKey = readSecret(request.user.directories, SECRET_KEYS.MOONSHOT);\n headers = {};\n bodyParams = {};\n request.body.json_schema\n ? setJsonObjectFormat(bodyParams, request.body.messages, request.body.json_schema)\n : addAssistantPrefix(request.body.messages, [], 'partial');\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.COMETAPI) {\n apiUrl = API_COMETAPI;\n apiKey = readSecret(request.user.directories, SECRET_KEYS.COMETAPI);\n headers = {};\n bodyParams = {\n reasoning_effort: request.body.reasoning_effort,\n };\n throw new Error('This provider is temporarily disabled.');\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.ZAI) {\n apiUrl = request.body.zai_endpoint === ZAI_ENDPOINT.CODING ? API_ZAI_CODING : API_ZAI_COMMON;\n apiKey = readSecret(request.user.directories, SECRET_KEYS.ZAI);\n headers = {\n 'Accept-Language': 'en-US,en',\n };\n bodyParams = {\n thinking: {\n type: request.body.include_reasoning ? 'enabled' : 'disabled',\n },\n };\n if (request.body.json_schema) {\n setJsonObjectFormat(bodyParams, request.body.messages, request.body.json_schema);\n }\n } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.SILICONFLOW) {\n apiUrl = API_SILICONFLOW;\n apiKey = readSecret(request.user.directories, SECRET_KEYS.SILICONFLOW);\n headers = {};\n bodyParams = {};\n if (request.body.json_schema) {\n setJsonObjectFormat(bodyParams, request.body.messages, request.body.json_schema);\n }\n } else {\n console.warn('This chat completion source is not supported yet.');\n return response.status(400).send({ error: true });\n }\n\n // A few of OpenAIs reasoning models support reasoning effort\n if (request.body.reasoning_effort && [CHAT_COMPLETION_SOURCES.CUSTOM, CHAT_COMPLETION_SOURCES.OPENAI].includes(request.body.chat_completion_source)) {\n if (OPENAI_REASONING_EFFORT_MODELS.includes(request.body.model)) {\n bodyParams['reasoning_effort'] = OPENAI_REASONING_EFFORT_MAP[request.body.reasoning_effort] ?? request.body.reasoning_effort;\n }\n }\n\n if (request.body.verbosity && [CHAT_COMPLETION_SOURCES.CUSTOM, CHAT_COMPLETION_SOURCES.OPENAI].includes(request.body.chat_completion_source)) {\n if (OPENAI_VERBOSITY_MODELS.test(request.body.model)) {\n bodyParams['verbosity'] = request.body.verbosity;\n }\n }\n\n if (!apiKey && !request.body.reverse_proxy && request.body.chat_completion_source !== CHAT_COMPLETION_SOURCES.CUSTOM) {\n console.warn('OpenAI API key is missing.');\n return response.status(400).send({ error: true });\n }\n\n // Add custom stop sequences\n if (Array.isArray(request.body.stop) && request.body.stop.length > 0) {\n bodyParams['stop'] = request.body.stop;\n }\n\n const textPrompt = isTextCompletion ? convertTextCompletionPrompt(request.body.messages) : '';\n const endpointUrl = isTextCompletion && request.body.chat_completion_source !== CHAT_COMPLETION_SOURCES.OPENROUTER ?\n `${apiUrl}/completions` :\n `${apiUrl}/chat/completions`;\n\n const controller = new AbortController();\n request.socket.removeAllListeners('close');\n request.socket.on('close', function () {\n controller.abort();\n });\n\n if (!isTextCompletion && Array.isArray(request.body.tools) && request.body.tools.length > 0) {\n bodyParams['tools'] = request.body.tools;\n bodyParams['tool_choice'] = request.body.tool_choice;\n }\n\n if (request.body.json_schema && !bodyParams['response_format']) {\n bodyParams['response_format'] = {\n type: 'json_schema',\n json_schema: {\n name: request.body.json_schema.name,\n strict: request.body.json_schema.strict ?? true,\n schema: request.body.json_schema.value,\n },\n };\n }\n\n const requestBody = {\n 'messages': isTextCompletion === false ? request.body.messages : undefined,\n 'prompt': isTextCompletion === true ? textPrompt : undefined,\n 'model': request.body.model,\n 'temperature': request.body.temperature,\n 'max_tokens': request.body.max_tokens,\n 'max_completion_tokens': request.body.max_completion_tokens,\n 'stream': request.body.stream,\n 'presence_penalty': request.body.presence_penalty,\n 'frequency_penalty': request.body.frequency_penalty,\n 'top_p': request.body.top_p,\n 'top_k': request.body.top_k,\n 'stop': isTextCompletion === false ? request.body.stop : undefined,\n 'logit_bias': request.body.logit_bias,\n 'seed': request.body.seed,\n 'n': request.body.n,\n ...bodyParams,\n };\n\n if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.CUSTOM) {\n excludeKeysByYaml(requestBody, request.body.custom_exclude_body);\n }\n\n /** @type {import('node-fetch').RequestInit} */\n const config = {\n method: 'post',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer ' + apiKey,\n ...headers,\n },\n body: JSON.stringify(requestBody),\n signal: controller.signal,\n };\n\n console.debug('Chat Completion request:', requestBody);\n\n const fetchResponse = await fetch(endpointUrl, config);\n\n if (request.body.stream) {\n console.info('Streaming request in progress');\n return forwardFetchResponse(fetchResponse, response);\n }\n\n if (fetchResponse.ok) {\n /** @type {any} */\n const json = await fetchResponse.json();\n console.debug('Chat Completion response:', json);\n return response.send(json);\n } else {\n const responseText = await fetchResponse.text();\n const errorData = tryParse(responseText);\n\n const message = fetchResponse.statusText || 'Unknown error occurred';\n const quota_error = fetchResponse.status === 429 && errorData?.error?.type === 'insufficient_quota';\n console.error('Chat completion request error: ', message, responseText);\n\n if (!response.headersSent) {\n response.send({ error: { message }, quota_error: quota_error });\n } else if (!response.writableEnded) {\n response.write(responseText);\n } else {\n response.end();\n }\n }\n } catch (error) {\n console.error('Generation failed', error);\n const message = error.code === 'ECONNREFUSED'\n ? `Connection refused: ${error.message}`\n : error.message || 'Unknown error occurred';\n\n if (!response.headersSent) {\n response.status(502).send({ error: { message, ...error } });\n } else {\n response.end();\n }\n }\n});\n\nconst multimodalModels = express.Router();\n\nmultimodalModels.post('/pollinations', async (_req, res) => {\n try {\n const response = await fetch('https://text.pollinations.ai/models');\n\n if (!response.ok) {\n return res.json([]);\n }\n\n /** @type {any} */\n const data = await response.json();\n\n if (!Array.isArray(data)) {\n return res.json([]);\n }\n\n const multimodalModels = data.filter(m => m?.vision).map(m => m.name);\n return res.json(multimodalModels);\n } catch (error) {\n console.error(error);\n return res.sendStatus(500);\n }\n});\n\nmultimodalModels.post('/aimlapi', async (_req, res) => {\n try {\n const response = await fetch('https://api.aimlapi.com/v1/models');\n\n if (!response.ok) {\n return res.json([]);\n }\n\n /** @type {any} */\n const data = await response.json();\n\n if (!Array.isArray(data?.data)) {\n return res.json([]);\n }\n\n const multimodalModels = data.data.filter(m => m?.features?.includes('openai/chat-completion.vision')).map(m => m.id);\n return res.json(multimodalModels);\n } catch (error) {\n console.error(error);\n return res.sendStatus(500);\n }\n});\n\nmultimodalModels.post('/nanogpt', async (_req, res) => {\n try {\n const response = await fetch('https://nano-gpt.com/api/v1/models?detailed=true');\n\n if (!response.ok) {\n return res.json([]);\n }\n\n /** @type {any} */\n const data = await response.json();\n\n if (!Array.isArray(data?.data)) {\n return res.json([]);\n }\n\n const multimodalModels = data.data.filter(m => m?.capabilities?.vision).map(m => m.id);\n return res.json(multimodalModels);\n } catch (error) {\n console.error(error);\n return res.sendStatus(500);\n }\n});\n\nmultimodalModels.post('/electronhub', async (_req, res) => {\n try {\n const response = await fetch('https://api.electronhub.ai/v1/models');\n\n if (!response.ok) {\n return res.json([]);\n }\n\n /** @type {any} */\n const data = await response.json();\n const multimodalModels = data.data.filter(m => m.metadata?.vision).map(m => m.id);\n return res.json(multimodalModels);\n } catch (error) {\n console.error(error);\n return res.sendStatus(500);\n }\n});\n\nmultimodalModels.post('/chutes', async (req, res) => {\n try {\n const key = readSecret(req.user.directories, SECRET_KEYS.CHUTES);\n\n if (!key) {\n return res.json([]);\n }\n\n const response = await fetch('https://llm.chutes.ai/v1/models', {\n headers: {\n 'Authorization': `Bearer ${key}`,\n },\n });\n\n if (!response.ok) {\n return res.json([]);\n }\n\n const data = await response.json();\n\n const modelsData = /** @type {{object: string, data: Array<{id: string, input_modalities?: string[]}>}} */ (data);\n const multimodalModels = modelsData.data\n .filter(m => m.input_modalities?.includes('image'))\n .map(m => m.id);\n return res.json(multimodalModels);\n } catch (error) {\n console.error(error);\n return res.sendStatus(500);\n }\n});\n\nmultimodalModels.post('/mistral', async (req, res) => {\n try {\n const key = readSecret(req.user.directories, SECRET_KEYS.MISTRALAI);\n\n if (!key) {\n return res.json([]);\n }\n\n const response = await fetch('https://api.mistral.ai/v1/models', {\n headers: {\n 'Authorization': `Bearer ${key}`,\n },\n });\n\n if (!response.ok) {\n return res.json([]);\n }\n\n /** @type {any} */\n const data = await response.json();\n const multimodalModels = data.data.filter(m => m.capabilities?.vision).map(m => m.id);\n return res.json(multimodalModels);\n } catch (error) {\n console.error(error);\n return res.sendStatus(500);\n }\n});\n\nmultimodalModels.post('/xai', async (req, res) => {\n try {\n const key = readSecret(req.user.directories, SECRET_KEYS.XAI);\n\n if (!key) {\n return res.json([]);\n }\n\n // xAI's /models endpoint doesn't return modality info, so we must use /language-models instead\n const response = await fetch('https://api.x.ai/v1/language-models', {\n headers: {\n 'Authorization': `Bearer ${key}`,\n },\n });\n\n if (!response.ok) {\n return res.json([]);\n }\n\n /** @type {any} */\n const data = await response.json();\n const multimodalModels = data.models.filter(m => m.input_modalities?.includes('image')).map(m => m.id);\n if (!multimodalModels.includes('grok-4-0709')) {\n // The endpoint says it doesn't support images, but it does\n multimodalModels.push('grok-4-0709');\n }\n return res.json(multimodalModels);\n } catch (error) {\n console.error(error);\n return res.sendStatus(500);\n }\n});\n\nrouter.use('/multimodal-models', multimodalModels);\n\nrouter.post('/process', async function (request, response) {\n try {\n if (!Array.isArray(request.body.messages)) {\n return response.status(400).send({ error: 'Invalid messages format' });\n }\n\n if (!Object.values(PROMPT_PROCESSING_TYPE).includes(request.body.type)) {\n return response.status(400).send({ error: 'Unknown processing type' });\n }\n\n const messages = postProcessPrompt(request.body.messages, request.body.type, getPromptNames(request));\n return response.send({ messages });\n } catch (error) {\n console.error(error);\n return response.sendStatus(500);\n }\n});\n"}}}}