Caching Middleware
Learn how to create a caching middleware with Next.js and KV.
Let's create a simple chat interface that uses `LanguageModelMiddleware` to cache the assistant's responses in fast KV storage.
Run it locally
$ npm install aiClient
Let's create a simple chat interface that allows users to send messages to the assistant and receive responses. You will integrate the useChat hook from @ai-toolkit/react to stream responses.
'use client';
import { useChat } from '@ai-toolkit/react';
export default function Chat() {
const { messages, input, handleInputChange, handleSubmit, error } = useChat();
if (error) return <div>{error.message}</div>;
return (
<div className="flex flex-col w-full max-w-md py-24 mx-auto stretch">
<div className="space-y-4">
{messages.map(m => (
<div key={m.id} className="whitespace-pre-wrap">
<div>
<div className="font-bold">{m.role}</div>
{m.toolInvocations ? (
<pre>{JSON.stringify(m.toolInvocations, null, 2)}</pre>
) : (
<p>{m.content}</p>
)}
</div>
</div>
))}
</div>
<form onSubmit={handleSubmit}>
<input
className="fixed bottom-0 w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl"
value={input}
placeholder="Say something..."
onChange={handleInputChange}
/>
</form>
</div>
);
}
- 1'use client';
- 2import { useChat } from '@ai-toolkit/react';
- 3export default function Chat() {
- 4 const { messages, input, handleInputChange, handleSubmit, error } = useChat();
- 5 if (error) return <div>{error.message}</div>;
- 6 return (
- 7 <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch">
- 8 <div className="space-y-4">
- 9 {messages.map(m => (
- 10 <div key={m.id} className="whitespace-pre-wrap">
- 11 <div>
- 12 <div className="font-bold">{m.role}</div>
- 13 {m.toolInvocations ? (
- 14 <pre>{JSON.stringify(m.toolInvocations, null, 2)}</pre>
- 15 ) : (
- 16 <p>{m.content}</p>
- 17 )}
- 18 </div>
- 19 </div>
- 20 ))}
- 21 </div>
- 22 <form onSubmit={handleSubmit}>
- 23 <input
- 24 className="fixed bottom-0 w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl"
- 25 value={input}
- 26 placeholder="Say something..."
- 27 onChange={handleInputChange}
- 28 />
- 29 </form>
- 30 </div>
- 31 );
- 32}
Middleware
Next, you will create a LanguageModelMiddleware that caches the assistant's responses in KV storage.
LanguageModelMiddleware has two methods: wrapGenerate and wrapStream.
wrapGenerate is called when using `generateText` and `generateObject`, while wrapStream is called when using `streamText` and `streamObject`.
For wrapGenerate, you can cache the response directly.
Instead, for wrapStream, you cache an array of the stream parts, which can then be used with `simulateReadableStream` function to create a simulated ReadableStream that returns the cached response.
In this way, the cached response is returned chunk-by-chunk as if it were being generated by the model.
You can control the initial delay and delay between chunks by adjusting the initialDelayInMs and chunkDelayInMs parameters of simulateReadableStream.
import { Redis } from '@upstash/redis';
import {
type LanguageModelV1,
type LanguageModelV3Middleware,
type LanguageModelV1StreamPart,
simulateReadableStream,
} from 'ai-toolkit';
const redis = new Redis({
url: process.env.KV_URL,
token: process.env.KV_TOKEN,
});
export const cacheMiddleware: LanguageModelV3Middleware = {
wrapGenerate: async ({ doGenerate, params }) => {
const cacheKey = JSON.stringify(params);
const cached = (await redis.get(cacheKey)) as Awaited<
ReturnType<LanguageModelV1['doGenerate']>
> | null;
if (cached !== null) {
return {
...cached,
response: {
...cached.response,
timestamp: cached?.response?.timestamp
? new Date(cached?.response?.timestamp)
: undefined,
},
};
}
const result = await doGenerate();
redis.set(cacheKey, result);
return result;
},
wrapStream: async ({ doStream, params }) => {
const cacheKey = JSON.stringify(params);
// Check if the result is in the cache
const cached = await redis.get(cacheKey);
// If cached, return a simulated ReadableStream that yields the cached result
if (cached !== null) {
// Format the timestamps in the cached response
const formattedChunks = (cached as LanguageModelV1StreamPart[]).map(p => {
if (p.type === 'response-metadata' && p.timestamp) {
return { ...p, timestamp: new Date(p.timestamp) };
} else return p;
});
return {
stream: simulateReadableStream({
initialDelayInMs: 0,
chunkDelayInMs: 10,
chunks: formattedChunks,
}),
};
}
// If not cached, proceed with streaming
const { stream, ...rest } = await doStream();
const fullResponse: LanguageModelV1StreamPart[] = [];
const transformStream = new TransformStream<
LanguageModelV1StreamPart,
LanguageModelV1StreamPart
>({
transform(chunk, controller) {
fullResponse.push(chunk);
controller.enqueue(chunk);
},
flush() {
// Store the full response in the cache after streaming is complete
redis.set(cacheKey, fullResponse);
},
});
return {
stream: stream.pipeThrough(transformStream),
...rest,
};
},
};
<Note>
This example uses @upstash/redis to store and retrieve the assistant's
responses but you can use any KV storage provider you would like.
</Note>
- 1import { Redis } from '@upstash/redis';
- 2import {
- 3 type LanguageModelV1,
- 4 type LanguageModelV3Middleware,
- 5 type LanguageModelV1StreamPart,
- 6 simulateReadableStream,
- 7} from 'ai-toolkit';
- 8const redis = new Redis({
- 9 url: process.env.KV_URL,
- 10 token: process.env.KV_TOKEN,
- 11});
- 12export const cacheMiddleware: LanguageModelV3Middleware = {
- 13 wrapGenerate: async ({ doGenerate, params }) => {
- 14 const cacheKey = JSON.stringify(params);
- 15 const cached = (await redis.get(cacheKey)) as Awaited<
- 16 ReturnType<LanguageModelV1['doGenerate']>
- 17 > | null;
- 18 if (cached !== null) {
- 19 return {
- 20 ...cached,
- 21 response: {
- 22 ...cached.response,
- 23 timestamp: cached?.response?.timestamp
- 24 ? new Date(cached?.response?.timestamp)
- 25 : undefined,
- 26 },
- 27 };
- 28 }
- 29 const result = await doGenerate();
- 30 redis.set(cacheKey, result);
- 31 return result;
- 32 },
- 33 wrapStream: async ({ doStream, params }) => {
- 34 const cacheKey = JSON.stringify(params);
- 35 // Check if the result is in the cache
- 36 const cached = await redis.get(cacheKey);
- 37 // If cached, return a simulated ReadableStream that yields the cached result
- 38 if (cached !== null) {
- 39 // Format the timestamps in the cached response
- 40 const formattedChunks = (cached as LanguageModelV1StreamPart[]).map(p => {
- 41 if (p.type === 'response-metadata' && p.timestamp) {
- 42 return { ...p, timestamp: new Date(p.timestamp) };
- 43 } else return p;
- 44 });
- 45 return {
- 46 stream: simulateReadableStream({
- 47 initialDelayInMs: 0,
- 48 chunkDelayInMs: 10,
- 49 chunks: formattedChunks,
- 50 }),
- 51 };
- 52 }
- 53 // If not cached, proceed with streaming
- 54 const { stream, ...rest } = await doStream();
- 55 const fullResponse: LanguageModelV1StreamPart[] = [];
- 56 const transformStream = new TransformStream<
- 57 LanguageModelV1StreamPart,
- 58 LanguageModelV1StreamPart
- 59 >({
- 60 transform(chunk, controller) {
- 61 fullResponse.push(chunk);
- 62 controller.enqueue(chunk);
- 63 },
- 64 flush() {
- 65 // Store the full response in the cache after streaming is complete
- 66 redis.set(cacheKey, fullResponse);
- 67 },
- 68 });
- 69 return {
- 70 stream: stream.pipeThrough(transformStream),
- 71 ...rest,
- 72 };
- 73 },
- 74};
Server
Finally, you will create an API route for api/chat to handle the assistant's messages and responses. You can use your cache middleware by wrapping the model with wrapLanguageModel and passing the middleware as an argument.
import { cacheMiddleware } from '@/ai/middleware';
import { wrapLanguageModel, streamText, tool } from 'ai-toolkit';
import { z } from 'zod';
const wrappedModel = wrapLanguageModel({
model: 'openai/gpt-4o-mini',
middleware: cacheMiddleware,
});
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: wrappedModel,
messages,
tools: {
weather: tool({
description: 'Get the weather in a location',
inputSchema: z.object({
location: z.string().describe('The location to get the weather for'),
}),
execute: async ({ location }) => ({
location,
temperature: 72 + Math.floor(Math.random() * 21) - 10,
}),
}),
},
});
return result.toUIMessageStreamResponse();
}
- 1import { cacheMiddleware } from '@/ai/middleware';
- 2import { wrapLanguageModel, streamText, tool } from 'ai-toolkit';
- 3import { z } from 'zod';
- 4const wrappedModel = wrapLanguageModel({
- 5 model: 'openai/gpt-4o-mini',
- 6 middleware: cacheMiddleware,
- 7});
- 8export async function POST(req: Request) {
- 9 const { messages } = await req.json();
- 10 const result = streamText({
- 11 model: wrappedModel,
- 12 messages,
- 13 tools: {
- 14 weather: tool({
- 15 description: 'Get the weather in a location',
- 16 inputSchema: z.object({
- 17 location: z.string().describe('The location to get the weather for'),
- 18 }),
- 19 execute: async ({ location }) => ({
- 20 location,
- 21 temperature: 72 + Math.floor(Math.random() * 21) - 10,
- 22 }),
- 23 }),
- 24 },
- 25 });
- 26 return result.toUIMessageStreamResponse();
- 27}