Skip to content

Commit 9fad9c6

Browse files
authored
feat: [ENG-3265] cache layer for prompt bodies on AI Gateway (#5009)
* cache prompt bodies * warning * instant cache clearing on environment changes and deletes * remove redundant cache reset * production impl for prompt body caching * remove console logs * remove console log
1 parent 4eb491d commit 9fad9c6

File tree

5 files changed

+290
-4
lines changed

5 files changed

+290
-4
lines changed

valhalla/jawn/src/lib/clients/cloudflareKV.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,37 @@ export async function removeFromCache(key: string): Promise<void> {
132132
);
133133
}
134134

135+
export async function removeSecureCacheEntries(
136+
keys: string[]
137+
): Promise<void> {
138+
const namespaceId = process.env.CLOUDFLARE_KV_NAMESPACE_ID ?? "";
139+
const accountId = process.env.CLOUDFLARE_ACCOUNT_ID ?? "";
140+
141+
// since on worker, we have secure cache (both HMAC key1 and HMAC key2)
142+
await Promise.all(
143+
keys
144+
.filter((key) => Boolean(key))
145+
.map(async (key) => {
146+
const hashedKeys = await Promise.all([
147+
hashWithHmac(key, 1),
148+
hashWithHmac(key, 2),
149+
]);
150+
151+
await Promise.all(
152+
hashedKeys.map((hashedKey) =>
153+
cloudflare.kv.namespaces.values.delete(
154+
namespaceId,
155+
hashedKey,
156+
{
157+
account_id: accountId,
158+
}
159+
)
160+
)
161+
);
162+
})
163+
);
164+
}
165+
135166
export async function storeInCache(
136167
key: string,
137168
value: string,
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { MAX_RETRIES } from "./refetchKeys";
2+
import { removeSecureCacheEntries } from "./clients/cloudflareKV";
3+
import { ENVIRONMENT } from "./clients/constant";
4+
5+
type ResetPromptCacheParams = {
6+
orgId: string;
7+
promptId: string;
8+
versionId?: string;
9+
environment?: string;
10+
};
11+
12+
const buildPromptCacheKeys = ({
13+
orgId,
14+
promptId,
15+
versionId,
16+
environment,
17+
}: ResetPromptCacheParams): string[] => {
18+
const keys = new Set<string>();
19+
20+
// Default production scope
21+
if (!versionId && !environment) {
22+
keys.add(`prompt_version_${promptId}_prod_${orgId}`);
23+
}
24+
25+
if (versionId) {
26+
keys.add(`prompt_version_${promptId}_version:${versionId}_${orgId}`);
27+
keys.add(`prompt_body_${promptId}_${versionId}_${orgId}`);
28+
keys.add(`prompt_version_${promptId}_prod_${orgId}`);
29+
}
30+
31+
if (environment) {
32+
keys.add(`prompt_version_${promptId}_env:${environment}_${orgId}`);
33+
if (environment === "production") {
34+
keys.add(`prompt_version_${promptId}_prod_${orgId}`);
35+
}
36+
}
37+
38+
return Array.from(keys);
39+
};
40+
41+
async function resetPromptCacheDev(
42+
{ orgId, promptId, versionId, environment }: ResetPromptCacheParams,
43+
retries = MAX_RETRIES
44+
) {
45+
try {
46+
const res = await fetch(
47+
`${process.env.HELICONE_WORKER_API}/reset-prompt-cache/${orgId}`,
48+
{
49+
method: "POST",
50+
headers: {
51+
"Content-Type": "application/json",
52+
},
53+
body: JSON.stringify({
54+
promptId,
55+
versionId,
56+
environment,
57+
}),
58+
}
59+
);
60+
61+
if (!res.ok) {
62+
console.error(res);
63+
if (retries > 0) {
64+
await new Promise((resolve) =>
65+
setTimeout(resolve, 10_000 * (MAX_RETRIES - retries))
66+
);
67+
await resetPromptCacheDev(
68+
{ orgId, promptId, versionId, environment },
69+
retries - 1
70+
);
71+
}
72+
}
73+
} catch (error) {
74+
console.error(error);
75+
if (retries > 0) {
76+
await new Promise((resolve) =>
77+
setTimeout(resolve, 10_000 * (MAX_RETRIES - retries))
78+
);
79+
await resetPromptCacheDev(
80+
{ orgId, promptId, versionId, environment },
81+
retries - 1
82+
);
83+
}
84+
}
85+
}
86+
87+
export async function resetPromptCache(
88+
params: ResetPromptCacheParams
89+
): Promise<void> {
90+
if (!params.promptId || !params.orgId) {
91+
console.warn("Missing promptId or orgId when resetting prompt cache.");
92+
return;
93+
}
94+
95+
if (ENVIRONMENT === "production") {
96+
const cacheKeys = buildPromptCacheKeys(params);
97+
if (cacheKeys.length === 0) {
98+
return;
99+
}
100+
await removeSecureCacheEntries(cacheKeys);
101+
return;
102+
}
103+
104+
await resetPromptCacheDev(params);
105+
}

valhalla/jawn/src/managers/prompt/PromptManager.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ import { RequestManager } from "../request/RequestManager";
2828
import { S3Client } from "../../lib/shared/db/s3Client";
2929
import type { OpenAIChatRequest } from "@helicone-package/llm-mapper/mappers/openai/chat-v2";
3030
import { AuthParams } from "../../packages/common/auth/types";
31-
import { StringChain } from "lodash";
3231
import { Prompt2025Input } from "../../lib/db/ClickhouseWrapper";
32+
import { resetPromptCache as invalidatePromptCache } from "../../lib/resetPromptCache";
3333

3434

3535
const PROMPT_ID_LENGTH = 6;
@@ -50,6 +50,21 @@ export class Prompt2025Manager extends BaseManager {
5050
);
5151
}
5252

53+
private async resetPromptCache(params: {
54+
promptId: string;
55+
versionId?: string;
56+
environment?: string;
57+
}): Promise<void> {
58+
try {
59+
await invalidatePromptCache({
60+
orgId: this.authParams.organizationId,
61+
...params,
62+
});
63+
} catch (error) {
64+
console.error("Error resetting prompt cache:", error);
65+
}
66+
}
67+
5368
private generateRandomPromptId() : string {
5469
const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
5570
let result = '';
@@ -666,6 +681,12 @@ export class Prompt2025Manager extends BaseManager {
666681
if (updateEnvResult.error) {
667682
return err(updateEnvResult.error);
668683
}
684+
685+
await this.resetPromptCache({
686+
promptId: params.promptId,
687+
environment: params.environment,
688+
});
689+
669690
return ok(null);
670691
}
671692

@@ -709,6 +730,11 @@ export class Prompt2025Manager extends BaseManager {
709730
return err(result.error);
710731
}
711732

733+
// remove prod cache
734+
await this.resetPromptCache({
735+
promptId: params.promptId,
736+
});
737+
712738
return ok(null);
713739
}
714740

@@ -730,6 +756,11 @@ export class Prompt2025Manager extends BaseManager {
730756
return err(s3Result.error);
731757
}
732758

759+
await this.resetPromptCache({
760+
promptId: params.promptId,
761+
versionId: params.promptVersionId
762+
});
763+
733764
return ok(null);
734765
}
735766

worker/src/lib/managers/PromptManager.ts

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,51 @@ export class PromptManager {
1515
private env: Env
1616
) {}
1717

18+
private buildPromptVersionCacheKey(
19+
params: HeliconeChatCreateParams,
20+
orgId: string
21+
): string | null {
22+
if (!params.prompt_id) {
23+
return null;
24+
}
25+
26+
const scope = params.environment
27+
? `env:${params.environment}`
28+
: params.version_id
29+
? `version:${params.version_id}`
30+
: "prod";
31+
32+
return `prompt_version_${params.prompt_id}_${scope}_${orgId}`;
33+
}
34+
35+
private buildPromptBodyCacheKey(
36+
promptId: string,
37+
versionId: string,
38+
orgId: string
39+
): string {
40+
return `prompt_body_${promptId}_${versionId}_${orgId}`;
41+
}
42+
43+
private async getPromptVersionIdWithCache(
44+
params: HeliconeChatCreateParams,
45+
orgId: string
46+
): Promise<Result<string, string>> {
47+
const cacheKey = this.buildPromptVersionCacheKey(params, orgId);
48+
if (!cacheKey) {
49+
return this.promptStore.getPromptVersionId(params, orgId);
50+
}
51+
52+
return await getAndStoreInCache(
53+
cacheKey,
54+
this.env,
55+
async () => {
56+
return this.promptStore.getPromptVersionId(params, orgId);
57+
},
58+
300,
59+
false
60+
);
61+
}
62+
1863
async getSourcePromptBodyWithFetch(
1964
params: HeliconeChatCreateParams,
2065
orgId: string
@@ -27,14 +72,23 @@ export class PromptManager {
2772
string
2873
>
2974
> {
30-
const versionIdResult = await this.promptStore.getPromptVersionId(
75+
const versionIdResult = await this.getPromptVersionIdWithCache(
3176
params,
3277
orgId
3378
);
3479
if (isErr(versionIdResult)) return err(versionIdResult.error);
3580

81+
const promptId = params.prompt_id;
82+
if (!promptId) {
83+
return err("No prompt ID provided");
84+
}
85+
3686
return await getAndStoreInCache(
37-
`prompt_body_${versionIdResult.data}_${orgId}`,
87+
this.buildPromptBodyCacheKey(
88+
promptId,
89+
versionIdResult.data,
90+
orgId
91+
),
3892
this.env,
3993
async () => {
4094
try {
@@ -50,7 +104,7 @@ export class PromptManager {
50104
return err(`Error retrieving prompt body: ${error}`);
51105
}
52106
},
53-
undefined,
107+
300,
54108
false
55109
);
56110
}

worker/src/routers/api/apiRouter.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,71 @@ function getAPIRouterV1(
7878
}
7979
);
8080

81+
router.post(
82+
"/reset-prompt-cache/:orgId",
83+
async (
84+
{ params: { orgId } },
85+
requestWrapper: RequestWrapper,
86+
env: Env,
87+
ctx: ExecutionContext
88+
) => {
89+
if (env.ENVIRONMENT !== "development") {
90+
return new Response("not allowed", { status: 403 });
91+
}
92+
93+
const data = await requestWrapper.unsafeGetJson<{
94+
promptId: string;
95+
versionId?: string;
96+
environment?: string;
97+
}>();
98+
99+
if (!data || !data.promptId) {
100+
return new Response("promptId is required", { status: 400 });
101+
}
102+
103+
try {
104+
const { removeFromCache } = await import("../../lib/util/cache/secureCache");
105+
const cacheKeysToDelete: string[] = [];
106+
107+
if (data.versionId) {
108+
const promptBodyCacheKey = `prompt_body_${data.promptId}_${data.versionId}_${orgId}`;
109+
cacheKeysToDelete.push(promptBodyCacheKey);
110+
111+
const promptVersionCacheKey = `prompt_version_${data.promptId}_version:${data.versionId}_${orgId}`;
112+
cacheKeysToDelete.push(promptVersionCacheKey);
113+
}
114+
115+
if (data.environment) {
116+
const promptVersionCacheKey = `prompt_version_${data.promptId}_env:${data.environment}_${orgId}`;
117+
cacheKeysToDelete.push(promptVersionCacheKey);
118+
}
119+
120+
await Promise.all(
121+
cacheKeysToDelete.map(key =>
122+
removeFromCache(key, env)
123+
)
124+
);
125+
126+
return new Response(JSON.stringify({
127+
success: true,
128+
deletedKeys: cacheKeysToDelete
129+
}), {
130+
status: 200,
131+
headers: { "Content-Type": "application/json" }
132+
});
133+
} catch (error) {
134+
console.error("Error resetting prompt cache:", error);
135+
return new Response(JSON.stringify({
136+
success: false,
137+
error: error instanceof Error ? error.message : "Unknown error"
138+
}), {
139+
status: 500,
140+
headers: { "Content-Type": "application/json" }
141+
});
142+
}
143+
}
144+
)
145+
81146
router.post(
82147
"/mock-set-provider-keys/:orgId",
83148
async (

0 commit comments

Comments
 (0)