Skip to content
Open
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
76 changes: 57 additions & 19 deletions packages/cache/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ import { createCached } from '@ai-sdk-tools/cache';

const cached = createCached({
cache: Redis.createClient({ url: "redis://localhost:6379" }),
keyPrefix: "my-app:",
storeName: "my-app",
ttl: 30 * 60 * 1000,
});
```
Expand All @@ -100,7 +100,7 @@ import { createCached } from '@ai-sdk-tools/cache';

const cached = createCached({
cache: new IORedis("redis://localhost:6379"),
keyPrefix: "my-app:",
storeName: "my-app",
ttl: 30 * 60 * 1000,
});
```
Expand All @@ -124,18 +124,54 @@ Supported clients:

```typescript
const cached = createCached({
cache?: any; // Redis client (optional, defaults to LRU)
keyPrefix?: string; // Cache key prefix (default: "ai-tools-cache:")
ttl?: number; // Time to live in ms (default: 10min LRU, 30min Redis)
debug?: boolean; // Debug logging (default: false)
onHit?: (key: string) => void; // Cache hit callback
onMiss?: (key: string) => void; // Cache miss callback
cache?: any; // Redis client (optional, defaults to LRU)
storeName?: string; // Redis store name (default: "ai-tools-cache")
ttl?: number; // Time to live in ms (default: 10min LRU, 30min Redis)
toolName?: string; // Tool name prefix for cache keys
keySeparator?: string; // Separator between key components (default: ':')
cacheKeyContext?: () => string; // Generate dynamic context suffix (multi-tenant)
debug?: boolean; // Debug logging (default: false)
onHit?: (key: string) => void; // Cache hit callback
onMiss?: (key: string) => void; // Cache miss callback
});
```

**`toolName`** - Prefix cache keys with tool name to prevent collisions between tools using similar parameters.

**`keySeparator`** - Customize separator between key components (default: `':'`). Useful for specific storage backend requirements.

**`cacheKeyContext`** - Function to generate dynamic context suffix for multi-tenant apps. The context is appended to the end of the cache key with a separator in between. Isolates cache by user, team, or other contextual data.

## Per-Tool Cache Key Prefixing

When multiple tools use similar parameters, they can create identical cache keys causing collisions. Use `toolName` to namespace your cache keys:

```typescript
const cached = createCached({ cache: Redis.fromEnv() });

const weatherTool = cached(originalWeatherTool, {
toolName: 'weatherTool',
});

const translationTool = cached(originalTranslationTool, {
toolName: 'translationTool',
});
```

This creates a clear Redis key hierarchy:

```
// Without toolName (collision risk)
ai-tools-cache:{city:"NYC"}

// With toolName (no collisions)
ai-tools-cache:weatherTool:{city:"NYC"}
ai-tools-cache:translationTool:{city:"NYC"}
```

## Multi-Tenant Apps (Context-Aware Caching)

For apps with user/team context, just add `getContext` to the cache config:
For multi-tenant apps, isolate cache by user/team using `cacheKeyContext`:

```typescript
import { cached } from '@ai-sdk-tools/cache';
Expand All @@ -149,10 +185,10 @@ const burnRateAnalysisTool = tool({
}),
execute: async ({ from, to }) => {
// Your app's way of getting current user/team context
const currentUser = getCurrentUser(); // or useUser(), getSession(), etc.
const currentUser = getCurrentUser();

return await db.getBurnRate({
teamId: currentUser.teamId, // ← Context used here
teamId: currentUser.teamId,
from,
to,
});
Expand All @@ -161,11 +197,11 @@ const burnRateAnalysisTool = tool({

// Cache with context - that's it!
export const cachedBurnRateTool = cached(burnRateAnalysisTool, {
cacheKey: () => {
cacheKeyContext: () => {
const currentUser = getCurrentUser();
return `team:${currentUser.teamId}:user:${currentUser.id}`;
},
ttl: 30 * 60 * 1000, // 30 minutes
ttl: 30 * 60 * 1000,
});
```

Expand All @@ -185,15 +221,16 @@ const cacheBackend = createCacheBackend({
type: 'redis',
redis: {
client: Redis.createClient({ url: process.env.REDIS_URL }),
keyPrefix: 'my-app:',
storeName: 'my-app',
},
});

// Export configured cache function
export function cached<T extends Tool>(tool: T, options = {}) {
export function cached<T extends Tool>(tool: T, toolName?: string, options = {}) {
return baseCached(tool, {
store: cacheBackend,
cacheKey: () => {
toolName, // Per-tool prefix
cacheKeyContext: () => {
const currentUser = getCurrentUser();
return `team:${currentUser.teamId}:user:${currentUser.id}`;
},
Expand All @@ -203,9 +240,10 @@ export function cached<T extends Tool>(tool: T, options = {}) {
});
}

// Throughout your app
// Throughout your app - each tool gets its own prefix
import { cached } from '@/lib/cache';
export const myTool = cached(originalTool);
export const weatherTool = cached(originalWeatherTool, 'weatherTool');
export const translateTool = cached(originalTranslateTool, 'translateTool');
```

## Streaming Tools with Artifacts
Expand Down Expand Up @@ -335,4 +373,4 @@ Contributions are welcome! Please read our [contributing guide](../../CONTRIBUTI

## License

MIT © [AI SDK Tools](https://github.com/ai-sdk-tools/ai-sdk-tools)
MIT © [AI SDK Tools](https://github.com/ai-sdk-tools/ai-sdk-tools)
27 changes: 16 additions & 11 deletions packages/cache/src/backends/factory.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { CacheStore } from "../types";
import { LRUCacheStore, SimpleCacheStore } from "../cache-store";
import type { CacheStore } from "../types";
import { MemoryCacheStore } from "./memory";
import { RedisCacheStore } from "./redis";

Expand All @@ -12,45 +12,50 @@ export interface CacheBackendConfig {
defaultTTL?: number;
redis?: {
client: any;
keyPrefix?: string;
storeName?: string;
};
}

/**
* Factory function to create cache backends
*/
export function createCacheBackend<T = any>(config: CacheBackendConfig): CacheStore<T> {
export function createCacheBackend<T = any>(
config: CacheBackendConfig,
): CacheStore<T> {
let store: CacheStore<T>;

switch (config.type) {
case "memory":
store = new MemoryCacheStore<T>(config.maxSize);
break;

case "lru":
store = new LRUCacheStore<T>(config.maxSize);
break;

case "simple":
store = new SimpleCacheStore<T>(config.maxSize);
break;

case "redis":
if (!config.redis?.client) {
throw new Error("Redis client is required for redis cache backend");
}
store = new RedisCacheStore<T>(config.redis.client, config.redis.keyPrefix);
store = new RedisCacheStore<T>(
config.redis.client,
config.redis.storeName,
);
break;

default:
throw new Error(`Unknown cache backend type: ${(config as any).type}`);
}

// Add default TTL support if configured
if (config.defaultTTL) {
(store as any).getDefaultTTL = () => config.defaultTTL;
}

return store;
}

Expand Down
4 changes: 2 additions & 2 deletions packages/cache/src/backends/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { LRUCacheStore, SimpleCacheStore } from "../cache-store";
export { RedisCacheStore } from "./redis";
export { MemoryCacheStore } from "./memory";
export { createCacheBackend } from "./factory";
export { MemoryCacheStore } from "./memory";
export { RedisCacheStore } from "./redis";
28 changes: 19 additions & 9 deletions packages/cache/src/backends/redis.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { DEFAULT_CACHE_KEY_SEPARATOR, DEFAULT_STORE_NAME } from "../constants";
import type { CacheEntry, CacheStore } from "../types";

/**
Expand All @@ -8,9 +9,14 @@ export class RedisCacheStore<T = any> implements CacheStore<T> {
private redis: any;
private keyPrefix: string;

constructor(redisClient: any, keyPrefix = "ai-tools-cache:") {
constructor(redisClient: any, storeName = DEFAULT_STORE_NAME) {
this.redis = redisClient;
this.keyPrefix = keyPrefix;
// Append separator if storeName doesn't end with a common separator
const endsWithSeparator = /[:|\-_]$/.test(storeName);
this.keyPrefix =
storeName && !endsWithSeparator
? `${storeName}${DEFAULT_CACHE_KEY_SEPARATOR}`
: storeName;
}

private getKey(key: string): string {
Expand All @@ -21,12 +27,12 @@ export class RedisCacheStore<T = any> implements CacheStore<T> {
try {
const data = await this.redis.get(this.getKey(key));
if (!data) return undefined;

// Handle different Redis client return types
let jsonString: string;
if (typeof data === 'string') {
if (typeof data === "string") {
jsonString = data;
} else if (typeof data === 'object') {
} else if (typeof data === "object") {
// Some Redis clients return objects directly
return {
result: data.result,
Expand All @@ -37,7 +43,7 @@ export class RedisCacheStore<T = any> implements CacheStore<T> {
// Convert other types to string
jsonString = String(data);
}

const parsed = JSON.parse(jsonString);
return {
result: parsed.result,
Expand All @@ -57,21 +63,25 @@ export class RedisCacheStore<T = any> implements CacheStore<T> {
timestamp: entry.timestamp,
key: entry.key,
});

await this.redis.set(this.getKey(key), data);
} catch (error) {
console.warn(`Redis cache set error for key ${key}:`, error);
}
}

async setWithTTL(key: string, entry: CacheEntry<T>, ttlSeconds: number): Promise<void> {
async setWithTTL(
key: string,
entry: CacheEntry<T>,
ttlSeconds: number,
): Promise<void> {
try {
const data = JSON.stringify({
result: entry.result,
timestamp: entry.timestamp,
key: entry.key,
});

await this.redis.setex(this.getKey(key), ttlSeconds, data);
} catch (error) {
console.warn(`Redis cache setex error for key ${key}:`, error);
Expand Down
Loading