| 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 | if (request.body.json_schema?.value) { | 2003 | if (request.body.min_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 | switch (request.body.chat_completion_source) { | 2007 | if (request.body.top_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 | bodyParams['user'] = uuidv4(); | 2023 | bodyParams['route'] = '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 | if (request.body.min_p !== undefined) { | 2026 | if (request.body.reasoning_effort) { |
| 2004 | bodyParams['min_p'] = request.body.min_p; | 2027 | bodyParams['reasoning'] = { effort: request.body.reasoning_effort }; |
| 2005 | } | 2028 | } |
| 2006 | | 2029 | |
| 2007 | if (request.body.top_a !== undefined) { | 2030 | if (request.body.verbosity) { |
| 2008 | bodyParams['top_a'] = request.body.top_a; | 2031 | bodyParams['verbosity'] = request.body.verbosity; |
| 2009 | } | 2032 | } |
| 2010 | | 2033 | |
| 2011 | if (request.body.repetition_penalty !== undefined) { | 2034 | if (request.body.json_schema) { |
| 2012 | bodyParams['repetition_penalty'] = request.body.repetition_penalty; | 2035 | bodyParams['response_format'] = { |
| 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 | if (request.body.use_fallback) { | 2052 | if (isClaude3or4) { |
| 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 | if (!isTextCompletion && bodyParams.logprobs > 0) { | 2235 | if (Array.isArray(request.body.stop) && request.body.stop.length > 0) { |
| 2078 | bodyParams.top_logprobs = bodyParams.logprobs; | 2236 | bodyParams['stop'] = request.body.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 | bodyParams['response_format'] = { | 2256 | bodyParams['response_format'] = { |
| 2107 | type: 'json_schema', | 2257 | type: 'json_schema', |
| 2108 | json_schema: { | 2258 | json_schema: { |
| 2109 | name: request.body.json_schema.name, | 2259 | 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, | 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 | schema: request.body.json_schema.value, | 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 | if (request.body.verbosity && [CHAT_COMPLETION_SOURCES.CUSTOM, CHAT_COMPLETION_SOURCES.OPENAI].includes(request.body.chat_completion_source)) { | 2285 | if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.CUSTOM) { |
| 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 | bodyParams['response_format'] = { | 2290 | const config = { |
| 2257 | type: 'json_schema', | 2291 | method: 'post', |
| 2258 | json_schema: { | 2292 | headers: { |
| 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 | schema: request.body.json_schema.value, | 2295 | ...headers, |
| 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 | console.debug('Chat Completion request:', requestBody); | 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 | if (request.body.stream) { | 2310 | if (fetchResponse.ok) { |
| 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 | if (!response.headersSent) { | 2323 | if (!response.headersSent) { |
| 2337 | response.status(502).send({ error: { message, ...error } }); | 2324 | response.send({ error: { message }, quota_error: quota_error }); |
| | 2325 | } else if (!response.writableEnded) { |
| | 2326 | response.write(responseText); |
| 2338 | } else { | 2327 | } else { |
| 2339 | response.end(); | 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 | if (!response.headersSent) { | 2337 | if (!response.headersSent) { |
| 2356 | response.send({ error: { message }, quota_error: quota_error }); | 2338 | response.status(502).send({ error: { message, ...error } }); |
| 2357 | } else if (!response.writableEnded) { | | |
| 2358 | response.write(responseText); | | |
| 2359 | } else { | 2339 | } else { |
| 2360 | response.end(); | 2340 | response.end(); |
| 2361 | } | 2341 | } |