fix: caching system prompt functionality for OpenRouter Claude (#4872)
Signed| @@ -45,6 +45,7 @@ import { | ||
| 45 | 45 | addAssistantPrefix, |
| 46 | 46 | embedOpenRouterMedia, |
| 47 | 47 | addReasoningContentToToolCalls, |
| 48 | + cachingSystemPromptForOpenRouterClaude, | |
| 48 | 49 | } from '../../prompt-converters.js'; |
| 49 | 50 | |
| 50 | 51 | import { readSecret, SECRET_KEYS } from '../secrets.js'; |
| @@ -2041,15 +2042,23 @@ router.post('/generate', function (request, response) { | ||
| 2041 | 2042 | }; |
| 2042 | 2043 | } |
| 2043 | 2044 | |
| 2045 | + const enableSystemPromptCache = getConfigValue('claude.enableSystemPromptCache', false, 'boolean'); | |
| 2044 | 2046 | const cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1, 'number'); |
| 2045 | 2047 | const isClaude3or4 = /anthropic\/claude-(3|opus-4|sonnet-4|haiku-4)/.test(request.body.model); |
| 2046 | 2048 | const cacheTTL = getConfigValue('claude.extendedTTL', false, 'boolean') ? '1h' : '5m'; |
| 2047 | 2049 | if (Array.isArray(request.body.messages)) { |
| 2048 | 2050 | embedOpenRouterMedia(request.body.messages); |
| 2049 | - if (Number.isInteger(cachingAtDepth) && cachingAtDepth >= 0 && isClaude3or4) { | |
| 2051 | + | |
| 2052 | + if (isClaude3or4) { | |
| 2053 | + if (enableSystemPromptCache) { | |
| 2054 | + cachingSystemPromptForOpenRouterClaude(request.body.messages, cacheTTL); | |
| 2055 | + } | |
| 2056 | + | |
| 2057 | + if (Number.isInteger(cachingAtDepth) && cachingAtDepth >= 0) { | |
| 2050 | 2058 | cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth, cacheTTL); |
| 2051 | 2059 | } |
| 2052 | 2060 | } |
| 2061 | + } | |
| 2053 | 2062 | |
| 2054 | 2063 | const isGemini = /google\/gemini/.test(request.body.model); |
| 2055 | 2064 | if (isGemini) { |
| @@ -1027,6 +1027,51 @@ export function cachingAtDepthForOpenRouterClaude(messages, cachingAtDepth, ttl) | ||
| 1027 | 1027 | } |
| 1028 | 1028 | |
| 1029 | 1029 | /** |
| 1030 | + * Adds cache_control to the system prompt for OpenRouter Claude requests. | |
| 1031 | + * | |
| 1032 | + * @param {object[]} messages Array of messages | |
| 1033 | + * @param {string} ttl TTL value | |
| 1034 | + */ | |
| 1035 | +export function cachingSystemPromptForOpenRouterClaude(messages, ttl) { | |
| 1036 | + if (!Array.isArray(messages) || messages.length === 0) { | |
| 1037 | + return; | |
| 1038 | + } | |
| 1039 | + | |
| 1040 | + // Find the first system message | |
| 1041 | + const systemMessage = messages.find(msg => msg.role === 'system'); | |
| 1042 | + if (!systemMessage) { | |
| 1043 | + return; | |
| 1044 | + } | |
| 1045 | + | |
| 1046 | + // Check if it already has cache_control (at message level) | |
| 1047 | + if (systemMessage.cache_control) { | |
| 1048 | + return; | |
| 1049 | + } | |
| 1050 | + | |
| 1051 | + if (Array.isArray(systemMessage.content)) { | |
| 1052 | + const hasExistingCacheControl = systemMessage.content.some(part => part?.cache_control); | |
| 1053 | + if (hasExistingCacheControl) { | |
| 1054 | + return; | |
| 1055 | + } | |
| 1056 | + | |
| 1057 | + for (let i = systemMessage.content.length - 1; i >= 0; i--) { | |
| 1058 | + if (systemMessage.content[i]?.type === 'text') { | |
| 1059 | + systemMessage.content[i].cache_control = { type: 'ephemeral', ttl }; | |
| 1060 | + return; | |
| 1061 | + } | |
| 1062 | + } | |
| 1063 | + } else if (typeof systemMessage.content === 'string') { | |
| 1064 | + systemMessage.content = [ | |
| 1065 | + { | |
| 1066 | + type: 'text', | |
| 1067 | + text: systemMessage.content, | |
| 1068 | + cache_control: { type: 'ephemeral', ttl }, | |
| 1069 | + }, | |
| 1070 | + ]; | |
| 1071 | + } | |
| 1072 | +} | |
| 1073 | + | |
| 1074 | +/** | |
| 1030 | 1075 | * Calculate the Claude budget tokens for a given reasoning effort. |
| 1031 | 1076 | * @param {number} maxTokens Maximum tokens |
| 1032 | 1077 | * @param {string} reasoningEffort Reasoning effort |