Merge pull request #3085 from honey-tree/claude-caching-at-depth Claude caching at depth

54db4983f4663d77db79ec1246888a5791bdb619

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
3 files changed, +101 -1Ignore whitespace
default/config.yaml+8 -0
@@ -168,5 +168,13 @@ claude:
168 # (e.g {{random}} macro or lorebooks not as in-chat injections).168 # (e.g {{random}} macro or lorebooks not as in-chat injections).
169 # Otherwise, you'll just waste money on cache misses.169 # Otherwise, you'll just waste money on cache misses.
170 enableSystemPromptCache: false170 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# -- SERVER PLUGIN CONFIGURATION --179# -- SERVER PLUGIN CONFIGURATION --
172enableServerPlugins: false180enableServerPlugins: false
src/endpoints/backends/chat-completions.js+19 -1
@@ -26,6 +26,8 @@ import {
26 convertMistralMessages,26 convertMistralMessages,
27 convertAI21Messages,27 convertAI21Messages,
28 mergeMessages,28 mergeMessages,
29 cachingAtDepthForOpenRouterClaude,
30 cachingAtDepthForClaude,
29} from '../../prompt-converters.js';31} from '../../prompt-converters.js';
3032
31import { readSecret, SECRET_KEYS } from '../secrets.js';33import { readSecret, SECRET_KEYS } from '../secrets.js';
@@ -80,6 +82,11 @@ async function sendClaudeRequest(request, response) {
80 const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.CLAUDE);82 const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.CLAUDE);
81 const divider = '-'.repeat(process.stdout.columns);83 const divider = '-'.repeat(process.stdout.columns);
82 const enableSystemPromptCache = getConfigValue('claude.enableSystemPromptCache', false) && request.body.model.startsWith('claude-3');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 }
8390
84 if (!apiKey) {91 if (!apiKey) {
85 console.log(color.red(`Claude API key is missing.\n${divider}`));92 console.log(color.red(`Claude API key is missing.\n${divider}`));
@@ -138,9 +145,15 @@ async function sendClaudeRequest(request, response) {
138 requestBody.tools[requestBody.tools.length - 1]['cache_control'] = { type: 'ephemeral' };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 additionalHeaders['anthropic-beta'] = 'prompt-caching-2024-07-31';154 additionalHeaders['anthropic-beta'] = 'prompt-caching-2024-07-31';
143 }155 }
156
144 console.log('Claude request:', requestBody);157 console.log('Claude request:', requestBody);
145158
146 const generateResponse = await fetch(apiUrl + '/messages', {159 const generateResponse = await fetch(apiUrl + '/messages', {
@@ -876,6 +889,11 @@ router.post('/generate', jsonParser, function (request, response) {
876 if (request.body.use_fallback) {889 if (request.body.use_fallback) {
877 bodyParams['route'] = 'fallback';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 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.CUSTOM) {897 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.CUSTOM) {
880 apiUrl = request.body.custom_url;898 apiUrl = request.body.custom_url;
881 apiKey = readSecret(request.user.directories, SECRET_KEYS.CUSTOM);899 apiKey = readSecret(request.user.directories, SECRET_KEYS.CUSTOM);
src/prompt-converters.js+74 -0
@@ -718,3 +718,77 @@ export function convertTextCompletionPrompt(messages) {
718 });718 });
719 return messageStrings.join('\n') + '\nassistant:';719 return messageStrings.join('\n') + '\nassistant:';
720}720}
721
722export 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 */
756export 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}