[Electron Hub] Prompt Caching Support for Claude models (#4918) * Prompt Caching support Claude models * Prompt Caching support Claude models * Diff clean-up --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
Signed| @@ -87,6 +87,16 @@ const API_SILICONFLOW = 'https://api.siliconflow.com/v1'; | |||
| 87 | const API_OPENROUTER = 'https://openrouter.ai/api/v1'; | 87 | const API_OPENROUTER = 'https://openrouter.ai/api/v1'; |
| 88 | 88 | ||
| 89 | /** | 89 | /** |
| 90 | * Module-scoped Claude caching configuration values. | ||
| 91 | */ | ||
| 92 | const cacheTTL = getConfigValue('claude.extendedTTL', false, 'boolean') ? '1h' : '5m'; | ||
| 93 | const enableSystemPromptCache = getConfigValue('claude.enableSystemPromptCache', false, 'boolean'); | ||
| 94 | const cachingAtDepth = (() => { | ||
| 95 | const value = getConfigValue('claude.cachingAtDepth', -1, 'number'); | ||
| 96 | return Number.isInteger(value) && value >= 0 ? value : -1; | ||
| 97 | })(); | ||
| 98 | |||
| 99 | /** | ||
| 90 | * Cache for cacheable (writing) OpenRouter model IDs. | 100 | * Cache for cacheable (writing) OpenRouter model IDs. |
| 91 | * @type {string[]} | 101 | * @type {string[]} |
| 92 | */ | 102 | */ |
| @@ -194,12 +204,6 @@ async function sendClaudeRequest(request, response) { | |||
| 194 | const apiUrl = new URL(request.body.reverse_proxy || API_CLAUDE).toString(); | 204 | const apiUrl = new URL(request.body.reverse_proxy || API_CLAUDE).toString(); |
| 195 | const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.CLAUDE); | 205 | const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.CLAUDE); |
| 196 | const divider = '-'.repeat(process.stdout.columns); | 206 | const divider = '-'.repeat(process.stdout.columns); |
| 197 | const enableSystemPromptCache = getConfigValue('claude.enableSystemPromptCache', false, 'boolean'); | ||
| 198 | let cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1, 'number'); | ||
| 199 | // Disabled if not an integer or negative | ||
| 200 | if (!Number.isInteger(cachingAtDepth) || cachingAtDepth < 0) { | ||
| 201 | cachingAtDepth = -1; | ||
| 202 | } | ||
| 203 | 207 | ||
| 204 | if (!apiKey) { | 208 | if (!apiKey) { |
| 205 | console.warn(color.red(`Claude API key is missing.\n${divider}`)); | 209 | console.warn(color.red(`Claude API key is missing.\n${divider}`)); |
| @@ -221,7 +225,6 @@ async function sendClaudeRequest(request, response) { | |||
| 221 | 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); | 225 | 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); |
| 222 | const isLimitedSampling = /^claude-(opus-4-1|sonnet-4-5|haiku-4-5|opus-4-5)/.test(request.body.model); | 226 | const isLimitedSampling = /^claude-(opus-4-1|sonnet-4-5|haiku-4-5|opus-4-5)/.test(request.body.model); |
| 223 | const useVerbosity = /^claude-(opus-4-5)/.test(request.body.model); | 227 | const useVerbosity = /^claude-(opus-4-5)/.test(request.body.model); |
| 224 | const cacheTTL = getConfigValue('claude.extendedTTL', false, 'boolean') ? '1h' : '5m'; | ||
| 225 | let fixThinkingPrefill = false; | 228 | let fixThinkingPrefill = false; |
| 226 | // Add custom stop sequences | 229 | // Add custom stop sequences |
| 227 | const stopSequences = []; | 230 | const stopSequences = []; |
| @@ -1365,6 +1368,18 @@ async function sendElectronHubRequest(request, response) { | |||
| 1365 | }; | 1368 | }; |
| 1366 | } | 1369 | } |
| 1367 | 1370 | ||
| 1371 | const isClaude = /^claude-/.test(request.body.model); | ||
| 1372 | |||
| 1373 | if (Array.isArray(request.body.messages) && isClaude) { | ||
| 1374 | if (enableSystemPromptCache) { | ||
| 1375 | cachingSystemPromptForOpenRouter(request.body.messages, cacheTTL); | ||
| 1376 | } | ||
| 1377 | |||
| 1378 | if (cachingAtDepth !== -1) { | ||
| 1379 | cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth, cacheTTL); | ||
| 1380 | } | ||
| 1381 | } | ||
| 1382 | |||
| 1368 | const requestBody = { | 1383 | const requestBody = { |
| 1369 | 'messages': request.body.messages, | 1384 | 'messages': request.body.messages, |
| 1370 | 'model': request.body.model, | 1385 | 'model': request.body.model, |
| @@ -2101,11 +2116,7 @@ router.post('/generate', async function (request, response) { | |||
| 2101 | }; | 2116 | }; |
| 2102 | } | 2117 | } |
| 2103 | 2118 | ||
| 2104 | const enableSystemPromptCache = getConfigValue('claude.enableSystemPromptCache', false, 'boolean'); | 2119 | const isClaude = /^anthropic\/claude/.test(request.body.model); |
| 2105 | const cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1, 'number'); | ||
| 2106 | const isClaude3or4 = /anthropic\/claude-(3|opus-4|sonnet-4|haiku-4)/.test(request.body.model); | ||
| 2107 | const cacheTTL = getConfigValue('claude.extendedTTL', false, 'boolean') ? '1h' : '5m'; | ||
| 2108 | |||
| 2109 | const isGemini = /google\/gemini/.test(request.body.model); | 2120 | const isGemini = /google\/gemini/.test(request.body.model); |
| 2110 | const isCacheableGemini = isGemini && await isOpenRouterModelCacheable(request.body.model); | 2121 | const isCacheableGemini = isGemini && await isOpenRouterModelCacheable(request.body.model); |
| 2111 | const enableGeminiSystemPromptCache = getConfigValue('gemini.enableSystemPromptCache', false, 'boolean'); | 2122 | const enableGeminiSystemPromptCache = getConfigValue('gemini.enableSystemPromptCache', false, 'boolean'); |
| @@ -2114,12 +2125,12 @@ router.post('/generate', async function (request, response) { | |||
| 2114 | embedOpenRouterMedia(request.body.messages); | 2125 | embedOpenRouterMedia(request.body.messages); |
| 2115 | addOpenRouterSignatures(request.body.messages, request.body.model); | 2126 | addOpenRouterSignatures(request.body.messages, request.body.model); |
| 2116 | 2127 | ||
| 2117 | if (isClaude3or4) { | 2128 | if (isClaude) { |
| 2118 | if (enableSystemPromptCache) { | 2129 | if (enableSystemPromptCache) { |
| 2119 | cachingSystemPromptForOpenRouter(request.body.messages, cacheTTL); | 2130 | cachingSystemPromptForOpenRouter(request.body.messages, cacheTTL); |
| 2120 | } | 2131 | } |
| 2121 | 2132 | ||
| 2122 | if (Number.isInteger(cachingAtDepth) && cachingAtDepth >= 0) { | 2133 | if (cachingAtDepth !== -1) { |
| 2123 | cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth, cacheTTL); | 2134 | cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth, cacheTTL); |
| 2124 | } | 2135 | } |
| 2125 | } | 2136 | } |
| @@ -2214,10 +2225,9 @@ router.post('/generate', async function (request, response) { | |||
| 2214 | if (request.body.repetition_penalty !== undefined) { | 2225 | if (request.body.repetition_penalty !== undefined) { |
| 2215 | bodyParams['repetition_penalty'] = request.body.repetition_penalty; | 2226 | bodyParams['repetition_penalty'] = request.body.repetition_penalty; |
| 2216 | } | 2227 | } |
| 2217 | const enableSystemPromptCache = getConfigValue('claude.enableSystemPromptCache', false, 'boolean'); | 2228 | |
| 2218 | const isClaude3or4 = /claude-(3|opus-4|sonnet-4|haiku-4)/.test(request.body.model); | 2229 | const isClaude = /^claude-/.test(request.body.model); |
| 2219 | const cacheTTL = getConfigValue('claude.extendedTTL', false, 'boolean') ? '1h' : '5m'; | 2230 | if (enableSystemPromptCache && isClaude) { |
| 2220 | if (enableSystemPromptCache && isClaude3or4) { | ||
| 2221 | bodyParams['cache_control'] = { | 2231 | bodyParams['cache_control'] = { |
| 2222 | 'enabled': true, | 2232 | 'enabled': true, |
| 2223 | 'ttl': cacheTTL, | 2233 | 'ttl': cacheTTL, |