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

6fdeaa2cd9d6aedb40c36a7f4217a2433b247d46

Chanho Chung <hello@chungchan.dev>

Signed
2 files changed, +56 -2Ignore whitespace
src/endpoints/backends/chat-completions.js+11 -2
@@ -45,6 +45,7 @@ import {
45 addAssistantPrefix,45 addAssistantPrefix,
46 embedOpenRouterMedia,46 embedOpenRouterMedia,
47 addReasoningContentToToolCalls,47 addReasoningContentToToolCalls,
48 cachingSystemPromptForOpenRouterClaude,
48} from '../../prompt-converters.js';49} from '../../prompt-converters.js';
4950
50import { readSecret, SECRET_KEYS } from '../secrets.js';51import { readSecret, SECRET_KEYS } from '../secrets.js';
@@ -2041,13 +2042,21 @@ router.post('/generate', function (request, response) {
2041 };2042 };
2042 }2043 }
20432044
2045 const enableSystemPromptCache = getConfigValue('claude.enableSystemPromptCache', false, 'boolean');
2044 const cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1, 'number');2046 const cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1, 'number');
2045 const isClaude3or4 = /anthropic\/claude-(3|opus-4|sonnet-4|haiku-4)/.test(request.body.model);2047 const isClaude3or4 = /anthropic\/claude-(3|opus-4|sonnet-4|haiku-4)/.test(request.body.model);
2046 const cacheTTL = getConfigValue('claude.extendedTTL', false, 'boolean') ? '1h' : '5m';2048 const cacheTTL = getConfigValue('claude.extendedTTL', false, 'boolean') ? '1h' : '5m';
2047 if (Array.isArray(request.body.messages)) {2049 if (Array.isArray(request.body.messages)) {
2048 embedOpenRouterMedia(request.body.messages);2050 embedOpenRouterMedia(request.body.messages);
2049 if (Number.isInteger(cachingAtDepth) && cachingAtDepth >= 0 && isClaude3or4) {2051
2050 cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth, cacheTTL);2052 if (isClaude3or4) {
2053 if (enableSystemPromptCache) {
2054 cachingSystemPromptForOpenRouterClaude(request.body.messages, cacheTTL);
2055 }
2056
2057 if (Number.isInteger(cachingAtDepth) && cachingAtDepth >= 0) {
2058 cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth, cacheTTL);
2059 }
2051 }2060 }
2052 }2061 }
20532062
src/prompt-converters.js+45 -0
@@ -1027,6 +1027,51 @@ export function cachingAtDepthForOpenRouterClaude(messages, cachingAtDepth, ttl)
1027}1027}
10281028
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 */
1035export 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 * Calculate the Claude budget tokens for a given reasoning effort.1075 * Calculate the Claude budget tokens for a given reasoning effort.
1031 * @param {number} maxTokens Maximum tokens1076 * @param {number} maxTokens Maximum tokens
1032 * @param {string} reasoningEffort Reasoning effort1077 * @param {string} reasoningEffort Reasoning effort