Merge pull request #3085 from honey-tree/claude-caching-at-depth Claude caching at depth
Signed| @@ -168,5 +168,13 @@ claude: | ||
| 168 | 168 | # (e.g {{random}} macro or lorebooks not as in-chat injections). |
| 169 | 169 | # Otherwise, you'll just waste money on cache misses. |
| 170 | 170 | enableSystemPromptCache: false |
| 171 | + # Enables caching of the message history at depth (if supported). | |
| 172 | + # https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching | |
| 173 | + # -- IMPORTANT! -- | |
| 174 | + # Use with caution. Behavior may be unpredictable and no guarantees can or will be made. | |
| 175 | + # Set to an integer to specify the desired depth. 0 (which does NOT include the prefill) | |
| 176 | + # should be ideal for most use cases. | |
| 177 | + # Any value other than a non-negative integer will be ignored and caching at depth will not be enabled. | |
| 178 | + cachingAtDepth: -1 | |
| 171 | 179 | # -- SERVER PLUGIN CONFIGURATION -- |
| 172 | 180 | enableServerPlugins: false |
| @@ -26,6 +26,8 @@ import { | ||
| 26 | 26 | convertMistralMessages, |
| 27 | 27 | convertAI21Messages, |
| 28 | 28 | mergeMessages, |
| 29 | + cachingAtDepthForOpenRouterClaude, | |
| 30 | + cachingAtDepthForClaude, | |
| 29 | 31 | } from '../../prompt-converters.js'; |
| 30 | 32 | |
| 31 | 33 | import { readSecret, SECRET_KEYS } from '../secrets.js'; |
| @@ -80,6 +82,11 @@ async function sendClaudeRequest(request, response) { | ||
| 80 | 82 | const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.CLAUDE); |
| 81 | 83 | const divider = '-'.repeat(process.stdout.columns); |
| 82 | 84 | const enableSystemPromptCache = getConfigValue('claude.enableSystemPromptCache', false) && request.body.model.startsWith('claude-3'); |
| 85 | + let cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1); | |
| 86 | + // Disabled if not an integer or negative, or if the model doesn't support it | |
| 87 | + if (!Number.isInteger(cachingAtDepth) || cachingAtDepth < 0 || !request.body.model.startsWith('claude-3')) { | |
| 88 | + cachingAtDepth = -1; | |
| 89 | + } | |
| 83 | 90 | |
| 84 | 91 | if (!apiKey) { |
| 85 | 92 | console.log(color.red(`Claude API key is missing.\n${divider}`)); |
| @@ -138,9 +145,15 @@ async function sendClaudeRequest(request, response) { | ||
| 138 | 145 | requestBody.tools[requestBody.tools.length - 1]['cache_control'] = { type: 'ephemeral' }; |
| 139 | 146 | } |
| 140 | 147 | } |
| 141 | - if (enableSystemPromptCache) { | |
| 148 | + | |
| 149 | + if (cachingAtDepth !== -1) { | |
| 150 | + cachingAtDepthForClaude(convertedPrompt.messages, cachingAtDepth); | |
| 151 | + } | |
| 152 | + | |
| 153 | + if (enableSystemPromptCache || cachingAtDepth !== -1) { | |
| 142 | 154 | additionalHeaders['anthropic-beta'] = 'prompt-caching-2024-07-31'; |
| 143 | 155 | } |
| 156 | + | |
| 144 | 157 | console.log('Claude request:', requestBody); |
| 145 | 158 | |
| 146 | 159 | const generateResponse = await fetch(apiUrl + '/messages', { |
| @@ -876,6 +889,11 @@ router.post('/generate', jsonParser, function (request, response) { | ||
| 876 | 889 | if (request.body.use_fallback) { |
| 877 | 890 | bodyParams['route'] = 'fallback'; |
| 878 | 891 | } |
| 892 | + | |
| 893 | + let cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1); | |
| 894 | + if (Number.isInteger(cachingAtDepth) && cachingAtDepth >= 0 && request.body.model.startsWith('anthropic/claude-3')) { | |
| 895 | + cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth); | |
| 896 | + } | |
| 879 | 897 | } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.CUSTOM) { |
| 880 | 898 | apiUrl = request.body.custom_url; |
| 881 | 899 | apiKey = readSecret(request.user.directories, SECRET_KEYS.CUSTOM); |
| @@ -718,3 +718,77 @@ export function convertTextCompletionPrompt(messages) { | ||
| 718 | 718 | }); |
| 719 | 719 | return messageStrings.join('\n') + '\nassistant:'; |
| 720 | 720 | } |
| 721 | + | |
| 722 | +export function cachingAtDepthForClaude(messages, cachingAtDepth) { | |
| 723 | + let passedThePrefill = false; | |
| 724 | + let depth = 0; | |
| 725 | + let previousRoleName = ''; | |
| 726 | + | |
| 727 | + for (let i = messages.length - 1; i >= 0; i--) { | |
| 728 | + if (!passedThePrefill && messages[i].role === 'assistant') { | |
| 729 | + continue; | |
| 730 | + } | |
| 731 | + | |
| 732 | + passedThePrefill = true; | |
| 733 | + | |
| 734 | + if (messages[i].role !== previousRoleName) { | |
| 735 | + if (depth === cachingAtDepth || depth === cachingAtDepth + 2) { | |
| 736 | + const content = messages[i].content; | |
| 737 | + content[content.length - 1].cache_control = { type: 'ephemeral' }; | |
| 738 | + } | |
| 739 | + | |
| 740 | + if (depth === cachingAtDepth + 2) { | |
| 741 | + break; | |
| 742 | + } | |
| 743 | + | |
| 744 | + depth += 1; | |
| 745 | + previousRoleName = messages[i].role; | |
| 746 | + } | |
| 747 | + } | |
| 748 | +} | |
| 749 | + | |
| 750 | +/** | |
| 751 | + * Append cache_control headers to an OpenRouter request at depth. Directly modifies the | |
| 752 | + * messages array. | |
| 753 | + * @param {object[]} messages Array of messages | |
| 754 | + * @param {number} cachingAtDepth Depth at which caching is supposed to occur | |
| 755 | + */ | |
| 756 | +export function cachingAtDepthForOpenRouterClaude(messages, cachingAtDepth) { | |
| 757 | + //caching the prefill is a terrible idea in general | |
| 758 | + let passedThePrefill = false; | |
| 759 | + //depth here is the number of message role switches | |
| 760 | + let depth = 0; | |
| 761 | + let previousRoleName = ''; | |
| 762 | + for (let i = messages.length - 1; i >= 0; i--) { | |
| 763 | + if (!passedThePrefill && messages[i].role === 'assistant') { | |
| 764 | + continue; | |
| 765 | + } | |
| 766 | + | |
| 767 | + passedThePrefill = true; | |
| 768 | + | |
| 769 | + if (messages[i].role !== previousRoleName) { | |
| 770 | + if (depth === cachingAtDepth || depth === cachingAtDepth + 2) { | |
| 771 | + const content = messages[i].content; | |
| 772 | + if (typeof content === 'string') { | |
| 773 | + messages[i].content = [{ | |
| 774 | + type: 'text', | |
| 775 | + text: content, | |
| 776 | + cache_control: { type: 'ephemeral' }, | |
| 777 | + }]; | |
| 778 | + } else { | |
| 779 | + const contentPartCount = content.length; | |
| 780 | + content[contentPartCount - 1].cache_control = { | |
| 781 | + type: 'ephemeral', | |
| 782 | + }; | |
| 783 | + } | |
| 784 | + } | |
| 785 | + | |
| 786 | + if (depth === cachingAtDepth + 2) { | |
| 787 | + break; | |
| 788 | + } | |
| 789 | + | |
| 790 | + depth += 1; | |
| 791 | + previousRoleName = messages[i].role; | |
| 792 | + } | |
| 793 | + } | |
| 794 | +} | |