From 0059cf298e29a8ea148234c57bbca11a354c4d9a Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 08:42:01 +0000 Subject: [PATCH 1/6] fix(openrouter): preserve existing cache_control --- .../providers/openrouter/request-helpers.ts | 21 ++++ src/tests/openrouter-request-helpers.test.ts | 106 ++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 src/tests/openrouter-request-helpers.test.ts diff --git a/src/lib/providers/openrouter/request-helpers.ts b/src/lib/providers/openrouter/request-helpers.ts index 7fcdd00bf1..c98fdd8a1f 100644 --- a/src/lib/providers/openrouter/request-helpers.ts +++ b/src/lib/providers/openrouter/request-helpers.ts @@ -80,7 +80,28 @@ function setCacheControlOnResponsesMessage(message: OpenAI.Responses.ResponseInp } } +function isObjectRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null; +} + +function containsCacheControl(value: unknown): boolean { + if (Array.isArray(value)) { + return value.some(containsCacheControl); + } + if (!isObjectRecord(value)) { + return false; + } + if (Object.hasOwn(value, 'cache_control')) { + return true; + } + return Object.values(value).some(containsCacheControl); +} + export function addCacheBreakpoints(request: GatewayRequest) { + if (containsCacheControl(request.body)) { + console.debug('[addCacheBreakpoints] skipping because cache_control is already present'); + return; + } if ( request.kind === 'chat_completions' && Array.isArray(request.body.messages) && diff --git a/src/tests/openrouter-request-helpers.test.ts b/src/tests/openrouter-request-helpers.test.ts new file mode 100644 index 0000000000..3b77b641ab --- /dev/null +++ b/src/tests/openrouter-request-helpers.test.ts @@ -0,0 +1,106 @@ +import { describe, expect, test } from '@jest/globals'; +import { addCacheBreakpoints } from '@/lib/providers/openrouter/request-helpers'; +import type { GatewayRequest } from '@/lib/providers/openrouter/types'; + +describe('addCacheBreakpoints', () => { + test('adds a cache breakpoint to the last eligible chat completions message when none exist', () => { + const request: GatewayRequest = { + kind: 'chat_completions', + body: { + model: 'test-model', + messages: [ + { role: 'system', content: 'You are helpful.' }, + { role: 'user', content: 'First prompt' }, + { role: 'assistant', content: 'First response' }, + { + role: 'user', + content: [ + { type: 'text', text: 'Latest prompt' }, + { type: 'text', text: 'Latest detail' }, + ], + }, + ], + }, + }; + + addCacheBreakpoints(request); + + const lastMessage = request.body.messages.at(-1); + expect(Array.isArray(lastMessage?.content)).toBe(true); + expect(lastMessage?.content.at(-1)).toMatchObject({ + type: 'text', + text: 'Latest detail', + cache_control: { type: 'ephemeral' }, + }); + }); + + test('does nothing for chat completions requests when any cache_control is already present', () => { + const request: GatewayRequest = { + kind: 'chat_completions', + body: { + model: 'test-model', + messages: [ + { role: 'system', content: 'You are helpful.' }, + { + role: 'user', + content: [{ type: 'text', text: 'First prompt', cache_control: { type: 'ephemeral' } }], + }, + { role: 'assistant', content: 'First response' }, + { + role: 'user', + content: [ + { type: 'text', text: 'Latest prompt' }, + { type: 'text', text: 'Latest detail' }, + ], + }, + ], + }, + }; + + addCacheBreakpoints(request); + + const lastMessage = request.body.messages.at(-1); + expect(lastMessage).toMatchObject({ + role: 'user', + content: [ + { type: 'text', text: 'Latest prompt' }, + { type: 'text', text: 'Latest detail' }, + ], + }); + }); + + test('does nothing for responses requests when any cache_control is already present', () => { + const request: GatewayRequest = { + kind: 'responses', + body: { + model: 'test-model', + input: [ + { + type: 'message', + role: 'user', + content: [{ type: 'input_text', text: 'First prompt', cache_control: { type: 'ephemeral' } }], + }, + { + type: 'function_call_output', + call_id: 'call_123', + output: [ + { type: 'input_text', text: 'Tool output' }, + { type: 'input_text', text: 'Tool detail' }, + ], + }, + ], + }, + }; + + addCacheBreakpoints(request); + + const lastItem = request.body.input.at(-1); + expect(lastItem).toMatchObject({ + type: 'function_call_output', + output: [ + { type: 'input_text', text: 'Tool output' }, + { type: 'input_text', text: 'Tool detail' }, + ], + }); + }); +}); From bc7ee3406ead925e323161ab579bcc844704b2fc Mon Sep 17 00:00:00 2001 From: Christiaan Arnoldus Date: Wed, 1 Apr 2026 10:49:01 +0200 Subject: [PATCH 2/6] Only check messages --- src/lib/providers/openrouter/request-helpers.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/lib/providers/openrouter/request-helpers.ts b/src/lib/providers/openrouter/request-helpers.ts index c98fdd8a1f..5538afbe8c 100644 --- a/src/lib/providers/openrouter/request-helpers.ts +++ b/src/lib/providers/openrouter/request-helpers.ts @@ -98,14 +98,11 @@ function containsCacheControl(value: unknown): boolean { } export function addCacheBreakpoints(request: GatewayRequest) { - if (containsCacheControl(request.body)) { - console.debug('[addCacheBreakpoints] skipping because cache_control is already present'); - return; - } if ( request.kind === 'chat_completions' && Array.isArray(request.body.messages) && - request.body.messages.length > 1 + request.body.messages.length > 1 && + !containsCacheControl(request.body.messages) ) { const lastMessage = request.body.messages.findLast( msg => msg.role === 'user' || msg.role === 'tool' @@ -119,7 +116,8 @@ export function addCacheBreakpoints(request: GatewayRequest) { } else if ( request.kind === 'responses' && Array.isArray(request.body.input) && - request.body.input.length > 1 + request.body.input.length > 1 && + !containsCacheControl(request.body.input) ) { const lastMessage = request.body.input.findLast( msg => (msg.type === 'message' && msg.role === 'user') || msg.type === 'function_call_output' From 784941f5582a8aef4aa40d403d859eef67603305 Mon Sep 17 00:00:00 2001 From: Christiaan Arnoldus Date: Wed, 1 Apr 2026 10:55:09 +0200 Subject: [PATCH 3/6] Formatting --- src/tests/openrouter-request-helpers.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/tests/openrouter-request-helpers.test.ts b/src/tests/openrouter-request-helpers.test.ts index 3b77b641ab..4687cf4294 100644 --- a/src/tests/openrouter-request-helpers.test.ts +++ b/src/tests/openrouter-request-helpers.test.ts @@ -78,7 +78,9 @@ describe('addCacheBreakpoints', () => { { type: 'message', role: 'user', - content: [{ type: 'input_text', text: 'First prompt', cache_control: { type: 'ephemeral' } }], + content: [ + { type: 'input_text', text: 'First prompt', cache_control: { type: 'ephemeral' } }, + ], }, { type: 'function_call_output', From d355f133dfee0d96f90db0c81955f8cecbf942a1 Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 09:02:05 +0000 Subject: [PATCH 4/6] fix(openrouter): add messages API support to addCacheBreakpoints --- src/app/api/openrouter/[...path]/route.ts | 3 - .../providers/openrouter/request-helpers.ts | 8 ++ src/tests/openrouter-request-helpers.test.ts | 86 ++++++++++++++++--- 3 files changed, 81 insertions(+), 16 deletions(-) diff --git a/src/app/api/openrouter/[...path]/route.ts b/src/app/api/openrouter/[...path]/route.ts index 75d693b1d9..94faf0b9e2 100644 --- a/src/app/api/openrouter/[...path]/route.ts +++ b/src/app/api/openrouter/[...path]/route.ts @@ -173,9 +173,6 @@ export async function POST(request: NextRequest): Promise 1) { - body.cache_control = { type: 'ephemeral' }; - } requestBodyParsed = { kind: 'messages', body }; } else { const body: GatewayResponsesRequest = JSON.parse(requestBodyText); diff --git a/src/lib/providers/openrouter/request-helpers.ts b/src/lib/providers/openrouter/request-helpers.ts index 5538afbe8c..bf0bc6d629 100644 --- a/src/lib/providers/openrouter/request-helpers.ts +++ b/src/lib/providers/openrouter/request-helpers.ts @@ -128,6 +128,14 @@ export function addCacheBreakpoints(request: GatewayRequest) { ); setCacheControlOnResponsesMessage(lastMessage); } + } else if ( + request.kind === 'messages' && + request.body.messages.length > 1 && + !containsCacheControl(request.body.messages) + ) { + console.debug('[addCacheBreakpoints] setting cache breakpoint on messages request'); + // @ts-expect-error non-standard top-level cache_control extension + request.body.cache_control = { type: 'ephemeral' }; } } diff --git a/src/tests/openrouter-request-helpers.test.ts b/src/tests/openrouter-request-helpers.test.ts index 4687cf4294..2f035489de 100644 --- a/src/tests/openrouter-request-helpers.test.ts +++ b/src/tests/openrouter-request-helpers.test.ts @@ -25,9 +25,10 @@ describe('addCacheBreakpoints', () => { addCacheBreakpoints(request); - const lastMessage = request.body.messages.at(-1); - expect(Array.isArray(lastMessage?.content)).toBe(true); - expect(lastMessage?.content.at(-1)).toMatchObject({ + const lastContent = request.body.messages.at(-1)?.content; + expect(Array.isArray(lastContent)).toBe(true); + if (!Array.isArray(lastContent)) return; + expect(lastContent.at(-1)).toMatchObject({ type: 'text', text: 'Latest detail', cache_control: { type: 'ephemeral' }, @@ -43,7 +44,14 @@ describe('addCacheBreakpoints', () => { { role: 'system', content: 'You are helpful.' }, { role: 'user', - content: [{ type: 'text', text: 'First prompt', cache_control: { type: 'ephemeral' } }], + content: [ + { + type: 'text', + text: 'First prompt', + // @ts-expect-error non-standard cache_control extension + cache_control: { type: 'ephemeral' }, + }, + ], }, { role: 'assistant', content: 'First response' }, { @@ -59,14 +67,11 @@ describe('addCacheBreakpoints', () => { addCacheBreakpoints(request); - const lastMessage = request.body.messages.at(-1); - expect(lastMessage).toMatchObject({ - role: 'user', - content: [ - { type: 'text', text: 'Latest prompt' }, - { type: 'text', text: 'Latest detail' }, - ], - }); + const lastContent = request.body.messages.at(-1)?.content; + expect(lastContent).toEqual([ + { type: 'text', text: 'Latest prompt' }, + { type: 'text', text: 'Latest detail' }, + ]); }); test('does nothing for responses requests when any cache_control is already present', () => { @@ -79,7 +84,12 @@ describe('addCacheBreakpoints', () => { type: 'message', role: 'user', content: [ - { type: 'input_text', text: 'First prompt', cache_control: { type: 'ephemeral' } }, + { + type: 'input_text', + text: 'First prompt', + // @ts-expect-error non-standard cache_control extension + cache_control: { type: 'ephemeral' }, + }, ], }, { @@ -105,4 +115,54 @@ describe('addCacheBreakpoints', () => { ], }); }); + + test('adds top-level cache_control on messages request when none is present', () => { + const request: GatewayRequest = { + kind: 'messages', + body: { + model: 'anthropic/claude-sonnet-4-5', + max_tokens: 1024, + messages: [ + { role: 'user', content: 'First prompt' }, + { role: 'assistant', content: 'First response' }, + { role: 'user', content: 'Latest prompt' }, + ], + }, + }; + + addCacheBreakpoints(request); + + // @ts-expect-error non-standard top-level cache_control extension + expect(request.body.cache_control).toEqual({ type: 'ephemeral' }); + }); + + test('does nothing for messages request when any cache_control is already present', () => { + const request: GatewayRequest = { + kind: 'messages', + body: { + model: 'anthropic/claude-sonnet-4-5', + max_tokens: 1024, + messages: [ + { + role: 'user', + content: [ + { + type: 'text', + text: 'First prompt', + // @ts-expect-error non-standard cache_control extension + cache_control: { type: 'ephemeral' }, + }, + ], + }, + { role: 'assistant', content: 'First response' }, + { role: 'user', content: 'Latest prompt' }, + ], + }, + }; + + addCacheBreakpoints(request); + + // @ts-expect-error non-standard top-level cache_control extension + expect(request.body.cache_control).toBeUndefined(); + }); }); From e4a2cedf6d31f55b780ad8d1627eb055b9a59b6b Mon Sep 17 00:00:00 2001 From: Christiaan Arnoldus Date: Wed, 1 Apr 2026 11:27:06 +0200 Subject: [PATCH 5/6] Retain request level cache_control --- src/lib/providers/openrouter/request-helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/providers/openrouter/request-helpers.ts b/src/lib/providers/openrouter/request-helpers.ts index bf0bc6d629..7d10a9f331 100644 --- a/src/lib/providers/openrouter/request-helpers.ts +++ b/src/lib/providers/openrouter/request-helpers.ts @@ -131,10 +131,10 @@ export function addCacheBreakpoints(request: GatewayRequest) { } else if ( request.kind === 'messages' && request.body.messages.length > 1 && + !request.body.cache_control && !containsCacheControl(request.body.messages) ) { console.debug('[addCacheBreakpoints] setting cache breakpoint on messages request'); - // @ts-expect-error non-standard top-level cache_control extension request.body.cache_control = { type: 'ephemeral' }; } } From 5a266f18a1e16e3be61612e3b81c1f9da9824cd8 Mon Sep 17 00:00:00 2001 From: Christiaan Arnoldus Date: Wed, 1 Apr 2026 11:31:35 +0200 Subject: [PATCH 6/6] Fix type errors --- src/tests/openrouter-request-helpers.test.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/tests/openrouter-request-helpers.test.ts b/src/tests/openrouter-request-helpers.test.ts index 2f035489de..87bace7fa5 100644 --- a/src/tests/openrouter-request-helpers.test.ts +++ b/src/tests/openrouter-request-helpers.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from '@jest/globals'; import { addCacheBreakpoints } from '@/lib/providers/openrouter/request-helpers'; import type { GatewayRequest } from '@/lib/providers/openrouter/types'; +import type OpenAI from 'openai'; describe('addCacheBreakpoints', () => { test('adds a cache breakpoint to the last eligible chat completions message when none exist', () => { @@ -48,9 +49,8 @@ describe('addCacheBreakpoints', () => { { type: 'text', text: 'First prompt', - // @ts-expect-error non-standard cache_control extension cache_control: { type: 'ephemeral' }, - }, + } as OpenAI.ChatCompletionContentPartText, ], }, { role: 'assistant', content: 'First response' }, @@ -67,7 +67,8 @@ describe('addCacheBreakpoints', () => { addCacheBreakpoints(request); - const lastContent = request.body.messages.at(-1)?.content; + const lastContent = + request.kind === 'chat_completions' && request.body.messages.at(-1)?.content; expect(lastContent).toEqual([ { type: 'text', text: 'Latest prompt' }, { type: 'text', text: 'Latest detail' }, @@ -106,7 +107,7 @@ describe('addCacheBreakpoints', () => { addCacheBreakpoints(request); - const lastItem = request.body.input.at(-1); + const lastItem = request.kind === 'responses' && request.body.input?.at(-1); expect(lastItem).toMatchObject({ type: 'function_call_output', output: [ @@ -132,7 +133,6 @@ describe('addCacheBreakpoints', () => { addCacheBreakpoints(request); - // @ts-expect-error non-standard top-level cache_control extension expect(request.body.cache_control).toEqual({ type: 'ephemeral' }); }); @@ -149,7 +149,6 @@ describe('addCacheBreakpoints', () => { { type: 'text', text: 'First prompt', - // @ts-expect-error non-standard cache_control extension cache_control: { type: 'ephemeral' }, }, ], @@ -162,7 +161,6 @@ describe('addCacheBreakpoints', () => { addCacheBreakpoints(request); - // @ts-expect-error non-standard top-level cache_control extension expect(request.body.cache_control).toBeUndefined(); }); });