fix: caching system prompt functionality for OpenRouter Claude (#4872)

6fdeaa2cd9d6aedb40c36a7f4217a2433b247d46

Chanho Chung <hello@chungchan.dev>

Signed
2 files changed, +55 -1Showing whitespace changes
src/endpoints/backends/chat-completions.js+10 -1
@@ -45,6 +45,7 @@ import {
4545 addAssistantPrefix,
4646 embedOpenRouterMedia,
4747 addReasoningContentToToolCalls,
48+ cachingSystemPromptForOpenRouterClaude,
4849} from '../../prompt-converters.js';
4950
5051import { readSecret, SECRET_KEYS } from '../secrets.js';
@@ -2041,15 +2042,23 @@ router.post('/generate', function (request, response) {
20412042 };
20422043 }
20432044
2045+ const enableSystemPromptCache = getConfigValue('claude.enableSystemPromptCache', false, 'boolean');
20442046 const cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1, 'number');
20452047 const isClaude3or4 = /anthropic\/claude-(3|opus-4|sonnet-4|haiku-4)/.test(request.body.model);
20462048 const cacheTTL = getConfigValue('claude.extendedTTL', false, 'boolean') ? '1h' : '5m';
20472049 if (Array.isArray(request.body.messages)) {
20482050 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) {
20502058 cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth, cacheTTL);
20512059 }
20522060 }
2061+ }
20532062
20542063 const isGemini = /google\/gemini/.test(request.body.model);
20552064 if (isGemini) {
src/prompt-converters.js+45 -0
@@ -1027,6 +1027,51 @@ export function cachingAtDepthForOpenRouterClaude(messages, cachingAtDepth, ttl)
10271027}
10281028
10291029/**
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+/**
10301075 * Calculate the Claude budget tokens for a given reasoning effort.
10311076 * @param {number} maxTokens Maximum tokens
10321077 * @param {string} reasoningEffort Reasoning effort