Skip to content

Commit 0d8fcc3

Browse files
authored
provider key cache (#5453)
* provider key cache * better version
1 parent f735169 commit 0d8fcc3

File tree

3 files changed

+95
-31
lines changed

3 files changed

+95
-31
lines changed

worker/src/lib/ai-gateway/AttemptBuilder.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,19 @@ export class AttemptBuilder {
2424
private readonly pluginHandler: PluginHandler;
2525
private readonly tracer: DataDogTracer;
2626
private readonly traceContext: TraceContext | null;
27+
private readonly ctx: ExecutionContext;
2728

2829
constructor(
2930
private readonly providerKeysManager: ProviderKeysManager,
3031
private readonly env: Env,
3132
tracer: DataDogTracer,
32-
traceContext: TraceContext | null
33+
traceContext: TraceContext | null,
34+
ctx: ExecutionContext
3335
) {
3436
this.pluginHandler = new PluginHandler();
3537
this.tracer = tracer;
3638
this.traceContext = traceContext;
39+
this.ctx = ctx;
3740
}
3841

3942
async buildAttempts(
@@ -210,7 +213,8 @@ export class AttemptBuilder {
210213
providerData.provider,
211214
modelSpec.modelName,
212215
orgId,
213-
modelSpec.customUid
216+
modelSpec.customUid,
217+
this.ctx
214218
);
215219

216220
this.tracer.finishSpan(keySpan);
@@ -266,7 +270,8 @@ export class AttemptBuilder {
266270
modelSpec.provider as ModelProviderName,
267271
modelSpec.modelName,
268272
orgId,
269-
modelSpec.customUid
273+
modelSpec.customUid,
274+
this.ctx
270275
);
271276

272277
if (!userKey || !this.isByokEnabled(userKey)) {
@@ -337,7 +342,9 @@ export class AttemptBuilder {
337342
const heliconeKey = await this.providerKeysManager.getProviderKeyWithFetch(
338343
providerData.provider,
339344
providerData.config.providerModelId,
340-
this.env.HELICONE_ORG_ID
345+
this.env.HELICONE_ORG_ID,
346+
undefined,
347+
this.ctx
341348
);
342349

343350
this.tracer.finishSpan(ptbKeySpan);

worker/src/lib/ai-gateway/SimpleAIGateway.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@ export class SimpleAIGateway {
9595
providerKeysManager,
9696
env,
9797
tracer,
98-
traceContext
98+
traceContext,
99+
ctx
99100
);
100101
this.attemptExecutor = new AttemptExecutor(env, ctx, cacheProvider, tracer);
101102
}

worker/src/lib/managers/ProviderKeysManager.ts

Lines changed: 82 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -88,53 +88,109 @@ export class ProviderKeysManager {
8888
return JSON.parse(keys) as ProviderKey[];
8989
}
9090

91+
/**
92+
* Get provider key with read-through cache pattern.
93+
* Returns cached data immediately, always refreshes in background.
94+
*/
9195
async getProviderKeyWithFetch(
9296
provider: ModelProviderName,
9397
providerModelId: string,
9498
orgId: string,
95-
keyCuid?: string
99+
keyCuid?: string,
100+
ctx?: ExecutionContext
96101
): Promise<ProviderKey | null> {
102+
const cacheKey = `provider_keys_${orgId}`;
103+
const ttl = 43200; // 12 hours
104+
105+
// Get cached keys
97106
const keys = await this.getProviderKeys(orgId);
107+
108+
// Try to find key from cache
98109
const validKey = this.chooseProviderKey(
99110
keys ?? [],
100111
provider,
101112
providerModelId,
102113
keyCuid
103114
);
104115

105-
if (!validKey) {
106-
const keys = await this.store.getProviderKeysWithFetch(
116+
if (validKey) {
117+
// Cache hit - trigger background refresh and return immediately
118+
if (ctx) {
119+
ctx.waitUntil(
120+
this.fetchAndCacheProviderKey(
121+
provider,
122+
providerModelId,
123+
orgId,
124+
keyCuid,
125+
cacheKey,
126+
ttl
127+
)
128+
);
129+
}
130+
return validKey;
131+
}
132+
133+
// Cache miss - must wait for fetch
134+
return this.fetchAndCacheProviderKey(
135+
provider,
136+
providerModelId,
137+
orgId,
138+
keyCuid,
139+
cacheKey,
140+
ttl
141+
);
142+
}
143+
144+
/**
145+
* Fetch provider key from Supabase and update cache.
146+
* Used both for cache miss (awaited) and background refresh (fire-and-forget).
147+
*/
148+
private async fetchAndCacheProviderKey(
149+
provider: ModelProviderName,
150+
providerModelId: string,
151+
orgId: string,
152+
keyCuid: string | undefined,
153+
cacheKey: string,
154+
ttl: number
155+
): Promise<ProviderKey | null> {
156+
try {
157+
const fetchedKeys = await this.store.getProviderKeysWithFetch(
107158
provider,
108159
orgId,
109160
keyCuid
110161
);
111162

112-
if (!keys) return null;
163+
if (!fetchedKeys || fetchedKeys.length === 0) return null;
164+
165+
// Merge with existing cache
166+
const existingCached = await this.getProviderKeys(orgId);
167+
const existingKeys = existingCached ?? [];
168+
169+
// Dedupe by cuid (or provider if no cuid)
170+
const keyMap = new Map<string, ProviderKey>();
171+
for (const key of existingKeys) {
172+
const id = key.cuid ?? `${key.provider}`;
173+
keyMap.set(id, key);
174+
}
175+
for (const key of fetchedKeys) {
176+
const id = key.cuid ?? `${key.provider}`;
177+
keyMap.set(id, key);
178+
}
113179

114-
const existingKeys = await getFromKVCacheOnly(
115-
`provider_keys_${orgId}`,
180+
const mergedKeys = Array.from(keyMap.values());
181+
182+
await storeInCache(
183+
cacheKey,
184+
JSON.stringify(mergedKeys),
116185
this.env,
117-
43200 // 12 hours
186+
ttl,
187+
false // Don't use memory cache to avoid test contamination
118188
);
119-
if (existingKeys) {
120-
const existingKeysData = JSON.parse(existingKeys) as ProviderKey[];
121-
existingKeysData.push(...keys);
122-
await storeInCache(
123-
`provider_keys_${orgId}`,
124-
JSON.stringify(existingKeysData),
125-
this.env,
126-
43200 // 12 hours
127-
);
128-
} else {
129-
await storeInCache(
130-
`provider_keys_${orgId}`,
131-
JSON.stringify(keys),
132-
this.env,
133-
43200 // 12 hours
134-
);
135-
}
136-
return this.chooseProviderKey(keys, provider, providerModelId, keyCuid);
189+
190+
return this.chooseProviderKey(fetchedKeys, provider, providerModelId, keyCuid);
191+
} catch (e) {
192+
console.error(`Failed to fetch/cache provider key for ${orgId}:`, e);
193+
return null;
137194
}
138-
return validKey;
139195
}
140196
}

0 commit comments

Comments
 (0)