| 1933 | 1933 | } |
| 1934 | 1934 | }); |
| 1935 | 1935 | |
| 1936 | +router.post('/generate', async function (request, response) { |
| 1937 | + try { |
| 1938 | + if (!request.body) return response.status(400).send({ error: true }); |
| 1939 | + |
| 1940 | + const postProcessingType = request.body.custom_prompt_post_processing; |
| 1941 | + if (Array.isArray(request.body.messages) && postProcessingType) { |
| 1942 | + console.info('Applying custom prompt post-processing of type', postProcessingType); |
| 1943 | + request.body.messages = postProcessPrompt( |
| 1944 | + request.body.messages, |
| 1945 | + postProcessingType, |
| 1946 | + getPromptNames(request)); |
| 1947 | + } |
| 1948 | + |
| 1949 | + if (request.body.json_schema?.value) { |
| 1950 | + request.body.json_schema.value = flattenSchema(request.body.json_schema.value, request.body.chat_completion_source); |
| 1951 | + } |
| 1952 | + |
| 1953 | + switch (request.body.chat_completion_source) { |
| 1954 | + case CHAT_COMPLETION_SOURCES.CLAUDE: return await sendClaudeRequest(request, response); |
| 1955 | + case CHAT_COMPLETION_SOURCES.AI21: return await sendAI21Request(request, response); |
| 1956 | + case CHAT_COMPLETION_SOURCES.MAKERSUITE: return await sendMakerSuiteRequest(request, response); |
| 1957 | + case CHAT_COMPLETION_SOURCES.VERTEXAI: return await sendMakerSuiteRequest(request, response); |
| 1958 | + case CHAT_COMPLETION_SOURCES.MISTRALAI: return await sendMistralAIRequest(request, response); |
| 1959 | + case CHAT_COMPLETION_SOURCES.COHERE: return await sendCohereRequest(request, response); |
| 1960 | + case CHAT_COMPLETION_SOURCES.DEEPSEEK: return await sendDeepSeekRequest(request, response); |
| 1961 | + case CHAT_COMPLETION_SOURCES.AIMLAPI: return await sendAimlapiRequest(request, response); |
| 1962 | + case CHAT_COMPLETION_SOURCES.XAI: return await sendXaiRequest(request, response); |
| 1963 | + case CHAT_COMPLETION_SOURCES.CHUTES: return await sendChutesRequest(request, response); |
| 1964 | + case CHAT_COMPLETION_SOURCES.ELECTRONHUB: return await sendElectronHubRequest(request, response); |
| 1965 | + case CHAT_COMPLETION_SOURCES.AZURE_OPENAI: return await sendAzureOpenAIRequest(request, response); |
| 1966 | + } |
| 1967 | + |
| 1968 | + let apiUrl; |
| 1969 | + let apiKey; |
| 1970 | + let headers; |
| 1971 | + let bodyParams; |
| 1972 | + const isTextCompletion = Boolean(request.body.model && TEXT_COMPLETION_MODELS.includes(request.body.model)) || typeof request.body.messages === 'string'; |
| 1973 | + |
| 1974 | + if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.OPENAI) { |
| 1975 | + apiUrl = new URL(request.body.reverse_proxy || API_OPENAI).toString(); |
| 1976 | + apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.OPENAI); |
| 1977 | + headers = {}; |
| 1978 | + bodyParams = { |
| 1979 | + logprobs: request.body.logprobs, |
| 1980 | + top_logprobs: undefined, |
| 1981 | + }; |
| 1936 | 1982 | |
| 1937 | | -router.post('/generate', function (request, response) { |
| 1983 | + // Adjust logprobs params for Chat Completions API, which expects { top_logprobs: number; logprobs: boolean; } |
| 1938 | | - if (!request.body) return response.status(400).send({ error: true }); |
| 1984 | + if (!isTextCompletion && bodyParams.logprobs > 0) { |
| 1985 | + bodyParams.top_logprobs = bodyParams.logprobs; |
| 1986 | + bodyParams.logprobs = true; |
| 1987 | + } |
| 1939 | 1988 | |
| 1940 | | - const postProcessingType = request.body.custom_prompt_post_processing; |
| 1989 | + if (getConfigValue('openai.randomizeUserId', false, 'boolean')) { |
| 1941 | | - if (Array.isArray(request.body.messages) && postProcessingType) { |
| 1990 | + bodyParams['user'] = uuidv4(); |
| 1942 | | - console.info('Applying custom prompt post-processing of type', postProcessingType); |
| 1991 | + } |
| 1943 | | - request.body.messages = postProcessPrompt( |
| 1992 | + } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.OPENROUTER) { |
| 1944 | | - request.body.messages, |
| 1993 | + apiUrl = 'https://openrouter.ai/api/v1'; |
| 1945 | | - postProcessingType, |
| 1994 | + apiKey = readSecret(request.user.directories, SECRET_KEYS.OPENROUTER); |
| 1946 | | - getPromptNames(request)); |
| 1995 | + // OpenRouter needs to pass the Referer and X-Title: https://openrouter.ai/docs#requests |
| 1947 | | - } |
| 1996 | + headers = { ...OPENROUTER_HEADERS }; |
| 1997 | + bodyParams = { |
| 1998 | + 'transforms': getOpenRouterTransforms(request), |
| 1999 | + 'plugins': getOpenRouterPlugins(request), |
| 2000 | + 'include_reasoning': Boolean(request.body.include_reasoning), |
| 2001 | + }; |
| 1948 | 2002 | |
| 1949 | 2003 | if (request.body.json_schema?.valuemin_p !== undefined) { |
| 1950 | | - request.body.json_schema.value = flattenSchema(request.body.json_schema.value, request.body.chat_completion_source); |
| 2004 | + bodyParams['min_p'] = request.body.min_p; |
| 1951 | 2005 | } |
| 1952 | 2006 | |
| 1953 | 2007 | switch if (request.body.chat_completion_sourcetop_a !== undefined) { |
| 1954 | | - case CHAT_COMPLETION_SOURCES.CLAUDE: return sendClaudeRequest(request, response); |
| 2008 | + bodyParams['top_a'] = request.body.top_a; |
| 1955 | | - case CHAT_COMPLETION_SOURCES.AI21: return sendAI21Request(request, response); |
| 2009 | + } |
| 1956 | | - case CHAT_COMPLETION_SOURCES.MAKERSUITE: return sendMakerSuiteRequest(request, response); |
| 1957 | | - case CHAT_COMPLETION_SOURCES.VERTEXAI: return sendMakerSuiteRequest(request, response); |
| 1958 | | - case CHAT_COMPLETION_SOURCES.MISTRALAI: return sendMistralAIRequest(request, response); |
| 1959 | | - case CHAT_COMPLETION_SOURCES.COHERE: return sendCohereRequest(request, response); |
| 1960 | | - case CHAT_COMPLETION_SOURCES.DEEPSEEK: return sendDeepSeekRequest(request, response); |
| 1961 | | - case CHAT_COMPLETION_SOURCES.AIMLAPI: return sendAimlapiRequest(request, response); |
| 1962 | | - case CHAT_COMPLETION_SOURCES.XAI: return sendXaiRequest(request, response); |
| 1963 | | - case CHAT_COMPLETION_SOURCES.CHUTES: return sendChutesRequest(request, response); |
| 1964 | | - case CHAT_COMPLETION_SOURCES.ELECTRONHUB: return sendElectronHubRequest(request, response); |
| 1965 | | - case CHAT_COMPLETION_SOURCES.AZURE_OPENAI: return sendAzureOpenAIRequest(request, response); |
| 1966 | | - } |
| 1967 | 2010 | |
| 1968 | | - let apiUrl; |
| 2011 | + if (request.body.repetition_penalty !== undefined) { |
| 1969 | | - let apiKey; |
| 2012 | + bodyParams['repetition_penalty'] = request.body.repetition_penalty; |
| 1970 | | - let headers; |
| 2013 | + } |
| 1971 | | - let bodyParams; |
| 1972 | | - const isTextCompletion = Boolean(request.body.model && TEXT_COMPLETION_MODELS.includes(request.body.model)) || typeof request.body.messages === 'string'; |
| 1973 | | - |
| 1974 | | - if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.OPENAI) { |
| 1975 | | - apiUrl = new URL(request.body.reverse_proxy || API_OPENAI).toString(); |
| 1976 | | - apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.OPENAI); |
| 1977 | | - headers = {}; |
| 1978 | | - bodyParams = { |
| 1979 | | - logprobs: request.body.logprobs, |
| 1980 | | - top_logprobs: undefined, |
| 1981 | | - }; |
| 1982 | 2014 | |
| 1983 | | - // Adjust logprobs params for Chat Completions API, which expects { top_logprobs: number; logprobs: boolean; } |
| 2015 | + if (Array.isArray(request.body.provider) && request.body.provider.length > 0) { |
| 1984 | | - if (!isTextCompletion && bodyParams.logprobs > 0) { |
| 2016 | + bodyParams['provider'] = { |
| 1985 | | - bodyParams.top_logprobs = bodyParams.logprobs; |
| 2017 | + allow_fallbacks: request.body.allow_fallbacks ?? true, |
| 1986 | | - bodyParams.logprobs = true; |
| 2018 | + order: request.body.provider ?? [], |
| 1987 | | - } |
| 2019 | + }; |
| 2020 | + } |
| 1988 | 2021 | |
| 1989 | | - if (getConfigValue('openai.randomizeUserId', false, 'boolean')) { |
| 2022 | + if (request.body.use_fallback) { |
| 1990 | 2023 | bodyParams['userroute'] = uuidv4()'fallback'; |
| 1991 | 2024 | } |
| 1992 | | - } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.OPENROUTER) { |
| 1993 | | - apiUrl = 'https://openrouter.ai/api/v1'; |
| 1994 | | - apiKey = readSecret(request.user.directories, SECRET_KEYS.OPENROUTER); |
| 1995 | | - // OpenRouter needs to pass the Referer and X-Title: https://openrouter.ai/docs#requests |
| 1996 | | - headers = { ...OPENROUTER_HEADERS }; |
| 1997 | | - bodyParams = { |
| 1998 | | - 'transforms': getOpenRouterTransforms(request), |
| 1999 | | - 'plugins': getOpenRouterPlugins(request), |
| 2000 | | - 'include_reasoning': Boolean(request.body.include_reasoning), |
| 2001 | | - }; |
| 2002 | 2025 | |
| 2003 | 2026 | if (request.body.min_p !== undefinedreasoning_effort) { |
| 2004 | 2027 | bodyParams['min_preasoning'] = { effort: request.body.min_preasoning_effort }; |
| 2005 | 2028 | } |
| 2006 | 2029 | |
| 2007 | 2030 | if (request.body.top_a !== undefinedverbosity) { |
| 2008 | 2031 | bodyParams['top_averbosity'] = request.body.top_averbosity; |
| 2009 | 2032 | } |
| 2010 | 2033 | |
| 2011 | 2034 | if (request.body.repetition_penalty !== undefinedjson_schema) { |
| 2012 | 2035 | bodyParams['repetition_penaltyresponse_format'] = request.body.repetition_penalty;{ |
| 2013 | | - } |
| 2036 | + type: 'json_schema', |
| 2037 | + json_schema: { |
| 2038 | + name: request.body.json_schema.name, |
| 2039 | + strict: request.body.json_schema.strict ?? true, |
| 2040 | + schema: request.body.json_schema.value, |
| 2041 | + }, |
| 2042 | + }; |
| 2043 | + } |
| 2014 | 2044 | |
| 2015 | | - if (Array.isArray(request.body.provider) && request.body.provider.length > 0) { |
| 2045 | + const enableSystemPromptCache = getConfigValue('claude.enableSystemPromptCache', false, 'boolean'); |
| 2016 | | - bodyParams['provider'] = { |
| 2046 | + const cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1, 'number'); |
| 2017 | | - allow_fallbacks: request.body.allow_fallbacks ?? true, |
| 2047 | + const isClaude3or4 = /anthropic\/claude-(3|opus-4|sonnet-4|haiku-4)/.test(request.body.model); |
| 2018 | | - order: request.body.provider ?? [], |
| 2048 | + const cacheTTL = getConfigValue('claude.extendedTTL', false, 'boolean') ? '1h' : '5m'; |
| 2019 | | - }; |
| 2049 | + if (Array.isArray(request.body.messages)) { |
| 2020 | | - } |
| 2050 | + embedOpenRouterMedia(request.body.messages); |
| 2021 | 2051 | |
| 2022 | 2052 | if (request.body.use_fallbackisClaude3or4) { |
| 2023 | | - bodyParams['route'] = 'fallback'; |
| 2053 | + if (enableSystemPromptCache) { |
| 2024 | | - } |
| 2054 | + cachingSystemPromptForOpenRouterClaude(request.body.messages, cacheTTL); |
| 2055 | + } |
| 2025 | 2056 | |
| 2026 | | - if (request.body.reasoning_effort) { |
| 2057 | + if (Number.isInteger(cachingAtDepth) && cachingAtDepth >= 0) { |
| 2027 | | - bodyParams['reasoning'] = { effort: request.body.reasoning_effort }; |
| 2058 | + cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth, cacheTTL); |
| 2028 | 2059 | } |
| 2060 | + } |
| 2061 | + } |
| 2029 | 2062 | |
| 2030 | | - if (request.body.verbosity) { |
| 2063 | + const isGemini = /google\/gemini/.test(request.body.model); |
| 2031 | | - bodyParams['verbosity'] = request.body.verbosity; |
| 2064 | + if (isGemini) { |
| 2032 | | - } |
| 2065 | + bodyParams['safety_settings'] = GEMINI_SAFETY; |
| 2066 | + } |
| 2067 | + } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.CUSTOM) { |
| 2068 | + apiUrl = request.body.custom_url; |
| 2069 | + apiKey = readSecret(request.user.directories, SECRET_KEYS.CUSTOM); |
| 2070 | + headers = {}; |
| 2071 | + bodyParams = { |
| 2072 | + logprobs: request.body.logprobs, |
| 2073 | + top_logprobs: undefined, |
| 2074 | + }; |
| 2033 | 2075 | |
| 2034 | | - if (request.body.json_schema) { |
| 2076 | + // Adjust logprobs params for Chat Completions API, which expects { top_logprobs: number; logprobs: boolean; } |
| 2035 | | - bodyParams['response_format'] = { |
| 2077 | + if (!isTextCompletion && bodyParams.logprobs > 0) { |
| 2036 | | - type: 'json_schema', |
| 2078 | + bodyParams.top_logprobs = bodyParams.logprobs; |
| 2037 | | - json_schema: { |
| 2079 | + bodyParams.logprobs = true; |
| 2038 | | - name: request.body.json_schema.name, |
| 2080 | + } |
| 2039 | | - strict: request.body.json_schema.strict ?? true, |
| 2081 | + |
| 2040 | | - schema: request.body.json_schema.value, |
| 2082 | + mergeObjectWithYaml(bodyParams, request.body.custom_include_body); |
| 2083 | + mergeObjectWithYaml(headers, request.body.custom_include_headers); |
| 2084 | + } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.PERPLEXITY) { |
| 2085 | + apiUrl = API_PERPLEXITY; |
| 2086 | + apiKey = readSecret(request.user.directories, SECRET_KEYS.PERPLEXITY); |
| 2087 | + headers = {}; |
| 2088 | + bodyParams = { |
| 2089 | + reasoning_effort: request.body.reasoning_effort, |
| 2090 | + }; |
| 2091 | + request.body.messages = postProcessPrompt(request.body.messages, PROMPT_PROCESSING_TYPE.STRICT, getPromptNames(request)); |
| 2092 | + if (request.body.json_schema) { |
| 2093 | + bodyParams['response_format'] = { |
| 2094 | + type: 'json_schema', |
| 2095 | + json_schema: { |
| 2096 | + schema: request.body.json_schema.value, |
| 2097 | + }, |
| 2098 | + }; |
| 2099 | + } |
| 2100 | + } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.GROQ) { |
| 2101 | + apiUrl = API_GROQ; |
| 2102 | + apiKey = readSecret(request.user.directories, SECRET_KEYS.GROQ); |
| 2103 | + headers = {}; |
| 2104 | + bodyParams = {}; |
| 2105 | + if (request.body.json_schema) { |
| 2106 | + bodyParams['response_format'] = { |
| 2107 | + type: 'json_schema', |
| 2108 | + json_schema: { |
| 2109 | + name: request.body.json_schema.name, |
| 2110 | + description: request.body.json_schema.description, |
| 2111 | + schema: request.body.json_schema.value, |
| 2112 | + strict: request.body.json_schema.strict ?? true, |
| 2113 | + }, |
| 2114 | + }; |
| 2115 | + } |
| 2116 | + } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.FIREWORKS) { |
| 2117 | + apiUrl = API_FIREWORKS; |
| 2118 | + apiKey = readSecret(request.user.directories, SECRET_KEYS.FIREWORKS); |
| 2119 | + headers = {}; |
| 2120 | + bodyParams = {}; |
| 2121 | + if (request.body.json_schema) { |
| 2122 | + bodyParams['response_format'] = { |
| 2123 | + type: 'json_schema', |
| 2124 | + json_schema: { |
| 2125 | + name: request.body.json_schema.name, |
| 2126 | + description: request.body.json_schema.description, |
| 2127 | + schema: request.body.json_schema.value, |
| 2128 | + strict: request.body.json_schema.strict ?? true, |
| 2129 | + }, |
| 2130 | + }; |
| 2131 | + } |
| 2132 | + } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.NANOGPT) { |
| 2133 | + apiUrl = API_NANOGPT; |
| 2134 | + apiKey = readSecret(request.user.directories, SECRET_KEYS.NANOGPT); |
| 2135 | + headers = {}; |
| 2136 | + bodyParams = {}; |
| 2137 | + if (request.body.enable_web_search && !/:online$/.test(request.body.model)) { |
| 2138 | + request.body.model = `${request.body.model}:online`; |
| 2139 | + } |
| 2140 | + if (request.body.min_p !== undefined) { |
| 2141 | + bodyParams['min_p'] = request.body.min_p; |
| 2142 | + } |
| 2143 | + if (request.body.top_a !== undefined) { |
| 2144 | + bodyParams['top_a'] = request.body.top_a; |
| 2145 | + } |
| 2146 | + if (request.body.repetition_penalty !== undefined) { |
| 2147 | + bodyParams['repetition_penalty'] = request.body.repetition_penalty; |
| 2148 | + } |
| 2149 | + const enableSystemPromptCache = getConfigValue('claude.enableSystemPromptCache', false, 'boolean'); |
| 2150 | + const isClaude3or4 = /claude-(3|opus-4|sonnet-4|haiku-4)/.test(request.body.model); |
| 2151 | + const cacheTTL = getConfigValue('claude.extendedTTL', false, 'boolean') ? '1h' : '5m'; |
| 2152 | + if (enableSystemPromptCache && isClaude3or4) { |
| 2153 | + bodyParams['cache_control'] = { |
| 2154 | + 'enabled': true, |
| 2155 | + 'ttl': cacheTTL, |
| 2156 | + }; |
| 2157 | + } |
| 2158 | + } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.POLLINATIONS) { |
| 2159 | + apiUrl = API_POLLINATIONS; |
| 2160 | + apiKey = 'NONE'; |
| 2161 | + headers = { |
| 2162 | + 'Authorization': '', |
| 2163 | + }; |
| 2164 | + bodyParams = { |
| 2165 | + reasoning_effort: request.body.reasoning_effort, |
| 2166 | + private: true, |
| 2167 | + referrer: 'sillytavern', |
| 2168 | + seed: request.body.seed ?? Math.floor(Math.random() * 99999999), |
| 2169 | + }; |
| 2170 | + if (request.body.json_schema) { |
| 2171 | + setJsonObjectFormat(bodyParams, request.body.messages, request.body.json_schema); |
| 2172 | + } |
| 2173 | + } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.MOONSHOT) { |
| 2174 | + apiUrl = API_MOONSHOT; |
| 2175 | + apiKey = readSecret(request.user.directories, SECRET_KEYS.MOONSHOT); |
| 2176 | + headers = {}; |
| 2177 | + bodyParams = {}; |
| 2178 | + request.body.json_schema |
| 2179 | + ? setJsonObjectFormat(bodyParams, request.body.messages, request.body.json_schema) |
| 2180 | + : addAssistantPrefix(request.body.messages, [], 'partial'); |
| 2181 | + } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.COMETAPI) { |
| 2182 | + apiUrl = API_COMETAPI; |
| 2183 | + apiKey = readSecret(request.user.directories, SECRET_KEYS.COMETAPI); |
| 2184 | + headers = {}; |
| 2185 | + bodyParams = { |
| 2186 | + reasoning_effort: request.body.reasoning_effort, |
| 2187 | + }; |
| 2188 | + throw new Error('This provider is temporarily disabled.'); |
| 2189 | + } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.ZAI) { |
| 2190 | + apiUrl = request.body.zai_endpoint === ZAI_ENDPOINT.CODING ? API_ZAI_CODING : API_ZAI_COMMON; |
| 2191 | + apiKey = readSecret(request.user.directories, SECRET_KEYS.ZAI); |
| 2192 | + headers = { |
| 2193 | + 'Accept-Language': 'en-US,en', |
| 2194 | + }; |
| 2195 | + bodyParams = { |
| 2196 | + thinking: { |
| 2197 | + type: request.body.include_reasoning ? 'enabled' : 'disabled', |
| 2041 | 2198 | }, |
| 2042 | 2199 | }; |
| 2200 | + if (request.body.json_schema) { |
| 2201 | + setJsonObjectFormat(bodyParams, request.body.messages, request.body.json_schema); |
| 2202 | + } |
| 2203 | + } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.SILICONFLOW) { |
| 2204 | + apiUrl = API_SILICONFLOW; |
| 2205 | + apiKey = readSecret(request.user.directories, SECRET_KEYS.SILICONFLOW); |
| 2206 | + headers = {}; |
| 2207 | + bodyParams = {}; |
| 2208 | + if (request.body.json_schema) { |
| 2209 | + setJsonObjectFormat(bodyParams, request.body.messages, request.body.json_schema); |
| 2210 | + } |
| 2211 | + } else { |
| 2212 | + console.warn('This chat completion source is not supported yet.'); |
| 2213 | + return response.status(400).send({ error: true }); |
| 2043 | 2214 | } |
| 2044 | 2215 | |
| 2045 | | - const enableSystemPromptCache = getConfigValue('claude.enableSystemPromptCache', false, 'boolean'); |
| 2216 | + // A few of OpenAIs reasoning models support reasoning effort |
| 2046 | | - const cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1, 'number'); |
| 2217 | + if (request.body.reasoning_effort && [CHAT_COMPLETION_SOURCES.CUSTOM, CHAT_COMPLETION_SOURCES.OPENAI].includes(request.body.chat_completion_source)) { |
| 2047 | | - const isClaude3or4 = /anthropic\/claude-(3|opus-4|sonnet-4|haiku-4)/.test(request.body.model); |
| 2218 | + if (OPENAI_REASONING_EFFORT_MODELS.includes(request.body.model)) { |
| 2048 | | - const cacheTTL = getConfigValue('claude.extendedTTL', false, 'boolean') ? '1h' : '5m'; |
| 2219 | + bodyParams['reasoning_effort'] = OPENAI_REASONING_EFFORT_MAP[request.body.reasoning_effort] ?? request.body.reasoning_effort; |
| 2049 | | - if (Array.isArray(request.body.messages)) { |
| 2220 | + } |
| 2050 | | - embedOpenRouterMedia(request.body.messages); |
| 2221 | + } |
| 2051 | | - |
| 2052 | | - if (isClaude3or4) { |
| 2053 | | - if (enableSystemPromptCache) { |
| 2054 | | - cachingSystemPromptForOpenRouterClaude(request.body.messages, cacheTTL); |
| 2055 | | - } |
| 2056 | 2222 | |
| 2057 | | - if (Number.isInteger(cachingAtDepth) && cachingAtDepth >= 0) { |
| 2223 | + if (request.body.verbosity && [CHAT_COMPLETION_SOURCES.CUSTOM, CHAT_COMPLETION_SOURCES.OPENAI].includes(request.body.chat_completion_source)) { |
| 2058 | | - cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth, cacheTTL); |
| 2224 | + if (OPENAI_VERBOSITY_MODELS.test(request.body.model)) { |
| 2059 | | - } |
| 2225 | + bodyParams['verbosity'] = request.body.verbosity; |
| 2060 | 2226 | } |
| 2061 | 2227 | } |
| 2062 | 2228 | |
| 2063 | | - const isGemini = /google\/gemini/.test(request.body.model); |
| 2229 | + if (!apiKey && !request.body.reverse_proxy && request.body.chat_completion_source !== CHAT_COMPLETION_SOURCES.CUSTOM) { |
| 2064 | | - if (isGemini) { |
| 2230 | + console.warn('OpenAI API key is missing.'); |
| 2065 | | - bodyParams['safety_settings'] = GEMINI_SAFETY; |
| 2231 | + return response.status(400).send({ error: true }); |
| 2066 | 2232 | } |
| 2067 | | - } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.CUSTOM) { |
| 2068 | | - apiUrl = request.body.custom_url; |
| 2069 | | - apiKey = readSecret(request.user.directories, SECRET_KEYS.CUSTOM); |
| 2070 | | - headers = {}; |
| 2071 | | - bodyParams = { |
| 2072 | | - logprobs: request.body.logprobs, |
| 2073 | | - top_logprobs: undefined, |
| 2074 | | - }; |
| 2075 | 2233 | |
| 2076 | | - // Adjust logprobs params for Chat Completions API, which expects { top_logprobs: number; logprobs: boolean; } |
| 2234 | + // Add custom stop sequences |
| 2077 | 2235 | if (!isTextCompletionArray.isArray(request.body.stop) && bodyParamsrequest.logprobsbody.stop.length > 0) { |
| 2078 | 2236 | bodyParams.top_logprobs['stop'] = bodyParamsrequest.logprobsbody.stop; |
| 2079 | | - bodyParams.logprobs = true; |
| 2080 | 2237 | } |
| 2081 | 2238 | |
| 2082 | | - mergeObjectWithYaml(bodyParams, request.body.custom_include_body); |
| 2239 | + const textPrompt = isTextCompletion ? convertTextCompletionPrompt(request.body.messages) : ''; |
| 2083 | | - mergeObjectWithYaml(headers, request.body.custom_include_headers); |
| 2240 | + const endpointUrl = isTextCompletion && request.body.chat_completion_source !== CHAT_COMPLETION_SOURCES.OPENROUTER ? |
| 2084 | | - } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.PERPLEXITY) { |
| 2241 | + `${apiUrl}/completions` : |
| 2085 | | - apiUrl = API_PERPLEXITY; |
| 2242 | + `${apiUrl}/chat/completions`; |
| 2086 | | - apiKey = readSecret(request.user.directories, SECRET_KEYS.PERPLEXITY); |
| 2243 | + |
| 2087 | | - headers = {}; |
| 2244 | + const controller = new AbortController(); |
| 2088 | | - bodyParams = { |
| 2245 | + request.socket.removeAllListeners('close'); |
| 2089 | | - reasoning_effort: request.body.reasoning_effort, |
| 2246 | + request.socket.on('close', function () { |
| 2090 | | - }; |
| 2247 | + controller.abort(); |
| 2091 | | - request.body.messages = postProcessPrompt(request.body.messages, PROMPT_PROCESSING_TYPE.STRICT, getPromptNames(request)); |
| 2248 | + }); |
| 2092 | | - if (request.body.json_schema) { |
| 2249 | + |
| 2093 | | - bodyParams['response_format'] = { |
| 2250 | + if (!isTextCompletion && Array.isArray(request.body.tools) && request.body.tools.length > 0) { |
| 2094 | | - type: 'json_schema', |
| 2251 | + bodyParams['tools'] = request.body.tools; |
| 2095 | | - json_schema: { |
| 2252 | + bodyParams['tool_choice'] = request.body.tool_choice; |
| 2096 | | - schema: request.body.json_schema.value, |
| 2097 | | - }, |
| 2098 | | - }; |
| 2099 | 2253 | } |
| 2100 | | - } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.GROQ) { |
| 2254 | + |
| 2101 | | - apiUrl = API_GROQ; |
| 2255 | + if (request.body.json_schema && !bodyParams['response_format']) { |
| 2102 | | - apiKey = readSecret(request.user.directories, SECRET_KEYS.GROQ); |
| 2103 | | - headers = {}; |
| 2104 | | - bodyParams = {}; |
| 2105 | | - if (request.body.json_schema) { |
| 2106 | 2256 | bodyParams['response_format'] = { |
| 2107 | 2257 | type: 'json_schema', |
| 2108 | 2258 | json_schema: { |
| 2109 | 2259 | name: request.body.json_schema.name, |
| 2110 | | - description: request.body.json_schema.description, |
| 2111 | | - schema: request.body.json_schema.value, |
| 2112 | 2260 | strict: request.body.json_schema.strict ?? true, |
| 2113 | | - }, |
| 2114 | | - }; |
| 2115 | | - } |
| 2116 | | - } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.FIREWORKS) { |
| 2117 | | - apiUrl = API_FIREWORKS; |
| 2118 | | - apiKey = readSecret(request.user.directories, SECRET_KEYS.FIREWORKS); |
| 2119 | | - headers = {}; |
| 2120 | | - bodyParams = {}; |
| 2121 | | - if (request.body.json_schema) { |
| 2122 | | - bodyParams['response_format'] = { |
| 2123 | | - type: 'json_schema', |
| 2124 | | - json_schema: { |
| 2125 | | - name: request.body.json_schema.name, |
| 2126 | | - description: request.body.json_schema.description, |
| 2127 | 2261 | schema: request.body.json_schema.value, |
| 2128 | | - strict: request.body.json_schema.strict ?? true, |
| 2129 | 2262 | }, |
| 2130 | 2263 | }; |
| 2131 | 2264 | } |
| 2132 | | - } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.NANOGPT) { |
| 2133 | | - apiUrl = API_NANOGPT; |
| 2134 | | - apiKey = readSecret(request.user.directories, SECRET_KEYS.NANOGPT); |
| 2135 | | - headers = {}; |
| 2136 | | - bodyParams = {}; |
| 2137 | | - if (request.body.enable_web_search && !/:online$/.test(request.body.model)) { |
| 2138 | | - request.body.model = `${request.body.model}:online`; |
| 2139 | | - } |
| 2140 | | - if (request.body.min_p !== undefined) { |
| 2141 | | - bodyParams['min_p'] = request.body.min_p; |
| 2142 | | - } |
| 2143 | | - if (request.body.top_a !== undefined) { |
| 2144 | | - bodyParams['top_a'] = request.body.top_a; |
| 2145 | | - } |
| 2146 | | - if (request.body.repetition_penalty !== undefined) { |
| 2147 | | - bodyParams['repetition_penalty'] = request.body.repetition_penalty; |
| 2148 | | - } |
| 2149 | | - const enableSystemPromptCache = getConfigValue('claude.enableSystemPromptCache', false, 'boolean'); |
| 2150 | | - const isClaude3or4 = /claude-(3|opus-4|sonnet-4|haiku-4)/.test(request.body.model); |
| 2151 | | - const cacheTTL = getConfigValue('claude.extendedTTL', false, 'boolean') ? '1h' : '5m'; |
| 2152 | | - if (enableSystemPromptCache && isClaude3or4) { |
| 2153 | | - bodyParams['cache_control'] = { |
| 2154 | | - 'enabled': true, |
| 2155 | | - 'ttl': cacheTTL, |
| 2156 | | - }; |
| 2157 | | - } |
| 2158 | | - } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.POLLINATIONS) { |
| 2159 | | - apiUrl = API_POLLINATIONS; |
| 2160 | | - apiKey = 'NONE'; |
| 2161 | | - headers = { |
| 2162 | | - 'Authorization': '', |
| 2163 | | - }; |
| 2164 | | - bodyParams = { |
| 2165 | | - reasoning_effort: request.body.reasoning_effort, |
| 2166 | | - private: true, |
| 2167 | | - referrer: 'sillytavern', |
| 2168 | | - seed: request.body.seed ?? Math.floor(Math.random() * 99999999), |
| 2169 | | - }; |
| 2170 | | - if (request.body.json_schema) { |
| 2171 | | - setJsonObjectFormat(bodyParams, request.body.messages, request.body.json_schema); |
| 2172 | | - } |
| 2173 | | - } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.MOONSHOT) { |
| 2174 | | - apiUrl = API_MOONSHOT; |
| 2175 | | - apiKey = readSecret(request.user.directories, SECRET_KEYS.MOONSHOT); |
| 2176 | | - headers = {}; |
| 2177 | | - bodyParams = {}; |
| 2178 | | - request.body.json_schema |
| 2179 | | - ? setJsonObjectFormat(bodyParams, request.body.messages, request.body.json_schema) |
| 2180 | | - : addAssistantPrefix(request.body.messages, [], 'partial'); |
| 2181 | | - } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.COMETAPI) { |
| 2182 | | - apiUrl = API_COMETAPI; |
| 2183 | | - apiKey = readSecret(request.user.directories, SECRET_KEYS.COMETAPI); |
| 2184 | | - headers = {}; |
| 2185 | | - bodyParams = { |
| 2186 | | - reasoning_effort: request.body.reasoning_effort, |
| 2187 | | - }; |
| 2188 | | - throw new Error('This provider is temporarily disabled.'); |
| 2189 | | - } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.ZAI) { |
| 2190 | | - apiUrl = request.body.zai_endpoint === ZAI_ENDPOINT.CODING ? API_ZAI_CODING : API_ZAI_COMMON; |
| 2191 | | - apiKey = readSecret(request.user.directories, SECRET_KEYS.ZAI); |
| 2192 | | - headers = { |
| 2193 | | - 'Accept-Language': 'en-US,en', |
| 2194 | | - }; |
| 2195 | | - bodyParams = { |
| 2196 | | - thinking: { |
| 2197 | | - type: request.body.include_reasoning ? 'enabled' : 'disabled', |
| 2198 | | - }, |
| 2199 | | - }; |
| 2200 | | - if (request.body.json_schema) { |
| 2201 | | - setJsonObjectFormat(bodyParams, request.body.messages, request.body.json_schema); |
| 2202 | | - } |
| 2203 | | - } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.SILICONFLOW) { |
| 2204 | | - apiUrl = API_SILICONFLOW; |
| 2205 | | - apiKey = readSecret(request.user.directories, SECRET_KEYS.SILICONFLOW); |
| 2206 | | - headers = {}; |
| 2207 | | - bodyParams = {}; |
| 2208 | | - if (request.body.json_schema) { |
| 2209 | | - setJsonObjectFormat(bodyParams, request.body.messages, request.body.json_schema); |
| 2210 | | - } |
| 2211 | | - } else { |
| 2212 | | - console.warn('This chat completion source is not supported yet.'); |
| 2213 | | - return response.status(400).send({ error: true }); |
| 2214 | | - } |
| 2215 | 2265 | |
| 2216 | | - // A few of OpenAIs reasoning models support reasoning effort |
| 2266 | + const requestBody = { |
| 2217 | | - if (request.body.reasoning_effort && [CHAT_COMPLETION_SOURCES.CUSTOM, CHAT_COMPLETION_SOURCES.OPENAI].includes(request.body.chat_completion_source)) { |
| 2267 | + 'messages': isTextCompletion === false ? request.body.messages : undefined, |
| 2218 | | - if (OPENAI_REASONING_EFFORT_MODELS.includes(request.body.model)) { |
| 2268 | + 'prompt': isTextCompletion === true ? textPrompt : undefined, |
| 2219 | | - bodyParams['reasoning_effort'] = OPENAI_REASONING_EFFORT_MAP[request.body.reasoning_effort] ?? request.body.reasoning_effort; |
| 2269 | + 'model': request.body.model, |
| 2220 | | - } |
| 2270 | + 'temperature': request.body.temperature, |
| 2221 | | - } |
| 2271 | + 'max_tokens': request.body.max_tokens, |
| 2272 | + 'max_completion_tokens': request.body.max_completion_tokens, |
| 2273 | + 'stream': request.body.stream, |
| 2274 | + 'presence_penalty': request.body.presence_penalty, |
| 2275 | + 'frequency_penalty': request.body.frequency_penalty, |
| 2276 | + 'top_p': request.body.top_p, |
| 2277 | + 'top_k': request.body.top_k, |
| 2278 | + 'stop': isTextCompletion === false ? request.body.stop : undefined, |
| 2279 | + 'logit_bias': request.body.logit_bias, |
| 2280 | + 'seed': request.body.seed, |
| 2281 | + 'n': request.body.n, |
| 2282 | + ...bodyParams, |
| 2283 | + }; |
| 2222 | 2284 | |
| 2223 | 2285 | if (request.body.verbositychat_completion_source &&=== [CHAT_COMPLETION_SOURCES.CUSTOM, CHAT_COMPLETION_SOURCES.OPENAI].includes(request.body.chat_completion_source)) { |
| 2224 | | - if (OPENAI_VERBOSITY_MODELS.test(request.body.model)) { |
| 2286 | + excludeKeysByYaml(requestBody, request.body.custom_exclude_body); |
| 2225 | | - bodyParams['verbosity'] = request.body.verbosity; |
| 2226 | 2287 | } |
| 2227 | | - } |
| 2228 | | - |
| 2229 | | - if (!apiKey && !request.body.reverse_proxy && request.body.chat_completion_source !== CHAT_COMPLETION_SOURCES.CUSTOM) { |
| 2230 | | - console.warn('OpenAI API key is missing.'); |
| 2231 | | - return response.status(400).send({ error: true }); |
| 2232 | | - } |
| 2233 | | - |
| 2234 | | - // Add custom stop sequences |
| 2235 | | - if (Array.isArray(request.body.stop) && request.body.stop.length > 0) { |
| 2236 | | - bodyParams['stop'] = request.body.stop; |
| 2237 | | - } |
| 2238 | | - |
| 2239 | | - const textPrompt = isTextCompletion ? convertTextCompletionPrompt(request.body.messages) : ''; |
| 2240 | | - const endpointUrl = isTextCompletion && request.body.chat_completion_source !== CHAT_COMPLETION_SOURCES.OPENROUTER ? |
| 2241 | | - `${apiUrl}/completions` : |
| 2242 | | - `${apiUrl}/chat/completions`; |
| 2243 | | - |
| 2244 | | - const controller = new AbortController(); |
| 2245 | | - request.socket.removeAllListeners('close'); |
| 2246 | | - request.socket.on('close', function () { |
| 2247 | | - controller.abort(); |
| 2248 | | - }); |
| 2249 | | - |
| 2250 | | - if (!isTextCompletion && Array.isArray(request.body.tools) && request.body.tools.length > 0) { |
| 2251 | | - bodyParams['tools'] = request.body.tools; |
| 2252 | | - bodyParams['tool_choice'] = request.body.tool_choice; |
| 2253 | | - } |
| 2254 | 2288 | |
| 2255 | | - if (request.body.json_schema && !bodyParams['response_format']) { |
| 2289 | + /** @type {import('node-fetch').RequestInit} */ |
| 2256 | 2290 | bodyParams['response_format']const config = { |
| 2257 | 2291 | typemethod: 'json_schemapost', |
| 2258 | 2292 | json_schemaheaders: { |
| 2259 | | - name: request.body.json_schema.name, |
| 2293 | + 'Content-Type': 'application/json', |
| 2260 | | - strict: request.body.json_schema.strict ?? true, |
| 2294 | + 'Authorization': 'Bearer ' + apiKey, |
| 2261 | 2295 | schema: request.body.json_schema.valueheaders, |
| 2262 | 2296 | }, |
| 2297 | + body: JSON.stringify(requestBody), |
| 2298 | + signal: controller.signal, |
| 2263 | 2299 | }; |
| 2264 | | - } |
| 2265 | | - |
| 2266 | | - const requestBody = { |
| 2267 | | - 'messages': isTextCompletion === false ? request.body.messages : undefined, |
| 2268 | | - 'prompt': isTextCompletion === true ? textPrompt : undefined, |
| 2269 | | - 'model': request.body.model, |
| 2270 | | - 'temperature': request.body.temperature, |
| 2271 | | - 'max_tokens': request.body.max_tokens, |
| 2272 | | - 'max_completion_tokens': request.body.max_completion_tokens, |
| 2273 | | - 'stream': request.body.stream, |
| 2274 | | - 'presence_penalty': request.body.presence_penalty, |
| 2275 | | - 'frequency_penalty': request.body.frequency_penalty, |
| 2276 | | - 'top_p': request.body.top_p, |
| 2277 | | - 'top_k': request.body.top_k, |
| 2278 | | - 'stop': isTextCompletion === false ? request.body.stop : undefined, |
| 2279 | | - 'logit_bias': request.body.logit_bias, |
| 2280 | | - 'seed': request.body.seed, |
| 2281 | | - 'n': request.body.n, |
| 2282 | | - ...bodyParams, |
| 2283 | | - }; |
| 2284 | | - |
| 2285 | | - if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.CUSTOM) { |
| 2286 | | - excludeKeysByYaml(requestBody, request.body.custom_exclude_body); |
| 2287 | | - } |
| 2288 | | - |
| 2289 | | - /** @type {import('node-fetch').RequestInit} */ |
| 2290 | | - const config = { |
| 2291 | | - method: 'post', |
| 2292 | | - headers: { |
| 2293 | | - 'Content-Type': 'application/json', |
| 2294 | | - 'Authorization': 'Bearer ' + apiKey, |
| 2295 | | - ...headers, |
| 2296 | | - }, |
| 2297 | | - body: JSON.stringify(requestBody), |
| 2298 | | - signal: controller.signal, |
| 2299 | | - }; |
| 2300 | 2300 | |
| 2301 | 2301 | console.debug('Chat Completion request:', requestBody); |
| 2302 | 2302 | |
| 2303 | | - makeRequest(config, response, request); |
| 2303 | + const fetchResponse = await fetch(endpointUrl, config); |
| 2304 | 2304 | |
| 2305 | | - /** |
| 2305 | + if (request.body.stream) { |
| 2306 | | - * Makes a fetch request to the OpenAI API endpoint. |
| 2306 | + console.info('Streaming request in progress'); |
| 2307 | | - * @param {import('node-fetch').RequestInit} config Fetch config |
| 2307 | + return forwardFetchResponse(fetchResponse, response); |
| 2308 | | - * @param {express.Response} response Express response |
| 2308 | + } |
| 2309 | | - * @param {express.Request} request Express request |
| 2310 | | - */ |
| 2311 | | - async function makeRequest(config, response, request) { |
| 2312 | | - try { |
| 2313 | | - controller.signal.throwIfAborted(); |
| 2314 | | - const fetchResponse = await fetch(endpointUrl, config); |
| 2315 | 2309 | |
| 2316 | 2310 | if (request.bodyfetchResponse.streamok) { |
| 2317 | | - console.info('Streaming request in progress'); |
| 2311 | + /** @type {any} */ |
| 2318 | | - forwardFetchResponse(fetchResponse, response); |
| 2312 | + const json = await fetchResponse.json(); |
| 2319 | | - return; |
| 2313 | + console.debug('Chat Completion response:', json); |
| 2320 | | - } |
| 2314 | + return response.send(json); |
| 2315 | + } else { |
| 2316 | + const responseText = await fetchResponse.text(); |
| 2317 | + const errorData = tryParse(responseText); |
| 2321 | 2318 | |
| 2322 | | - if (fetchResponse.ok) { |
| 2319 | + const message = fetchResponse.statusText || 'Unknown error occurred'; |
| 2323 | | - /** @type {any} */ |
| 2320 | + const quota_error = fetchResponse.status === 429 && errorData?.error?.type === 'insufficient_quota'; |
| 2324 | | - let json = await fetchResponse.json(); |
| 2321 | + console.error('Chat completion request error: ', message, responseText); |
| 2325 | | - response.send(json); |
| 2326 | | - console.debug('Chat Completion response:', json); |
| 2327 | | - } else { |
| 2328 | | - await handleErrorResponse(fetchResponse); |
| 2329 | | - } |
| 2330 | | - } catch (error) { |
| 2331 | | - console.error('Generation failed', error); |
| 2332 | | - const message = error.code === 'ECONNREFUSED' |
| 2333 | | - ? `Connection refused: ${error.message}` |
| 2334 | | - : error.message || 'Unknown error occurred'; |
| 2335 | 2322 | |
| 2336 | 2323 | if (!response.headersSent) { |
| 2337 | 2324 | response.status(502).send({ error: { message }, ...errorquota_error: }quota_error }); |
| 2325 | + } else if (!response.writableEnded) { |
| 2326 | + response.write(responseText); |
| 2338 | 2327 | } else { |
| 2339 | 2328 | response.end(); |
| 2340 | 2329 | } |
| 2341 | 2330 | } |
| 2342 | | - } |
| 2331 | + } catch (error) { |
| 2343 | | - |
| 2332 | + console.error('Generation failed', error); |
| 2344 | | - /** |
| 2333 | + const message = error.code === 'ECONNREFUSED' |
| 2345 | | - * @param {import("node-fetch").Response} errorResponse |
| 2334 | + ? `Connection refused: ${error.message}` |
| 2346 | | - */ |
| 2335 | + : error.message || 'Unknown error occurred'; |
| 2347 | | - async function handleErrorResponse(errorResponse) { |
| 2348 | | - const responseText = await errorResponse.text(); |
| 2349 | | - const errorData = tryParse(responseText); |
| 2350 | | - |
| 2351 | | - const message = errorResponse.statusText || 'Unknown error occurred'; |
| 2352 | | - const quota_error = errorResponse.status === 429 && errorData?.error?.type === 'insufficient_quota'; |
| 2353 | | - console.error('Chat completion request error: ', message, responseText); |
| 2354 | 2336 | |
| 2355 | 2337 | if (!response.headersSent) { |
| 2356 | 2338 | response.status(502).send({ error: { message }, quota_error:...error quota_error} }); |
| 2357 | | - } else if (!response.writableEnded) { |
| 2358 | | - response.write(responseText); |
| 2359 | 2339 | } else { |
| 2360 | 2340 | response.end(); |
| 2361 | 2341 | } |