Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type Anthropic from '@anthropic-ai/sdk';
import type OpenAI from 'openai';
import type {
GatewayRequest,
GatewayResponsesRequest,
OpenCodeSpecificProperties,
OpenRouterChatCompletionRequest,
} from '@/lib/ai-gateway/providers/openrouter/types';
import type OpenAI from 'openai';

export function getMaxTokens(request: GatewayRequest) {
if (request.kind === 'responses') {
Expand Down Expand Up @@ -80,6 +81,41 @@ function setCacheControlOnResponsesMessage(message: OpenAI.Responses.ResponseInp
}
}

function setCacheControlOnMessagesMessage(
message: Anthropic.MessageParam,
cacheControl: Anthropic.CacheControlEphemeral
) {
if (typeof message.content === 'string') {
message.content = [
{
type: 'text',
text: message.content,
cache_control: cacheControl,
},
];
} else {
const lastItem = message.content.findLast(isCacheableMessagesContentBlock);
if (lastItem) {
lastItem.cache_control = cacheControl;
}
}
}

function isCacheableMessagesContentBlock(
item: Anthropic.ContentBlockParam
): item is Exclude<
Anthropic.ContentBlockParam,
Anthropic.ThinkingBlockParam | Anthropic.RedactedThinkingBlockParam
> {
return item.type !== 'thinking' && item.type !== 'redacted_thinking';
}

function hasCacheableMessagesContent(message: Anthropic.MessageParam) {
return typeof message.content === 'string'
? message.content.length > 0
: message.content.some(isCacheableMessagesContentBlock);
}

function isObjectRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}
Expand Down Expand Up @@ -163,11 +199,16 @@ 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');
request.body.cache_control = { type: 'ephemeral' };
const lastMessage = request.body.messages.findLast(hasCacheableMessagesContent);
if (lastMessage) {
console.debug('[addCacheBreakpoints] setting cache breakpoint on last messages message');
// Vercel AI Gateway does not honor top-level cache_control on Messages API requests.
const cacheControl = request.body.cache_control ?? { type: 'ephemeral' };
delete request.body.cache_control;
setCacheControlOnMessagesMessage(lastMessage, cacheControl);
}
}
}

Expand Down
68 changes: 66 additions & 2 deletions apps/web/src/tests/openrouter-request-helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ describe('addCacheBreakpoints', () => {
});
});

test('adds top-level cache_control on messages request when none is present', () => {
test('adds cache_control to the last content block of a messages request', () => {
const request: GatewayRequest = {
kind: 'messages',
body: {
Expand All @@ -223,7 +223,71 @@ describe('addCacheBreakpoints', () => {

addCacheBreakpoints(request);

expect(request.body.cache_control).toEqual({ type: 'ephemeral' });
expect(request.body.cache_control).toBeUndefined();
expect(request.body.messages.at(-1)?.content).toEqual([
{ type: 'text', text: 'Latest prompt', cache_control: { type: 'ephemeral' } },
]);
});

test('adds nested cache_control when an unhonored top-level value is present', () => {
const request: GatewayRequest = {
kind: 'messages',
body: {
model: 'anthropic/claude-sonnet-4-5',
max_tokens: 1024,
cache_control: { type: 'ephemeral', ttl: '1h' },
messages: [
{ 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);

expect(request.body.cache_control).toBeUndefined();
expect(request.body.messages.at(-1)?.content).toEqual([
{ type: 'text', text: 'Latest prompt' },
{
type: 'text',
text: 'Latest detail',
cache_control: { type: 'ephemeral', ttl: '1h' },
},
]);
});

test('adds cache_control to the last cacheable content block', () => {
const request: GatewayRequest = {
kind: 'messages',
body: {
model: 'anthropic/claude-sonnet-4-5',
max_tokens: 1024,
messages: [
{ role: 'user', content: 'Prompt' },
{
role: 'assistant',
content: [
{ type: 'text', text: 'Response' },
{ type: 'thinking', thinking: 'Reasoning', signature: 'signature' },
],
},
],
},
};

addCacheBreakpoints(request);

expect(request.body.messages.at(-1)?.content).toEqual([
{ type: 'text', text: 'Response', cache_control: { type: 'ephemeral' } },
{ type: 'thinking', thinking: 'Reasoning', signature: 'signature' },
]);
});

test('does nothing for messages request when any cache_control is already present', () => {
Expand Down