openai-cache 1.0.14 → 1.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # Cache OpenAI
2
2
  A simple caching layer for [OpenAI API](https://www.npmjs.com/package/openai), designed to reduce redundant API calls and save time and costs. It works by intercepting API requests and storing their responses in a cache. When the same request is made again, the cached response is returned instead of making a new API call.
3
3
 
4
+ It supports **text responses**, **binary responses** (e.g. images), and **streaming** (SSE).
5
+
4
6
  It is based on the [cacheable](https://cacheable.org/docs/) library, which provides a simple interface for caching data with support for various storage backends (like in-memory, Redis, SQLite, etc). This allows you to easily integrate caching into your OpenAI API usage without having to manage the caching logic yourself.
5
7
 
6
8
  You can use any Keyv storage backend (like Redis, filesystem, etc) to store the cached responses.
@@ -50,6 +52,12 @@ const response = await client.responses.create({
50
52
  console.log(response.output_text);
51
53
  ```
52
54
 
55
+ ## Environment Variables
56
+
57
+ | Variable | Values | Description |
58
+ |---|---|---|
59
+ | `OPENAI_CACHE` | `disabled` | Bypass the cache: responses are still written but never read from cache. Useful for testing/debugging without changing code. |
60
+
53
61
  ## PRO/CON
54
62
  - **PRO**: Reduces redundant API calls, saving time and costs.
55
63
  data.
@@ -0,0 +1,54 @@
1
+ import { Cacheable } from "cacheable";
2
+ type FetchFn = typeof globalThis.fetch;
3
+ type FetchInput = Parameters<FetchFn>[0];
4
+ type FetchInit = Parameters<FetchFn>[1];
5
+ type FetchResponse = Awaited<ReturnType<FetchFn>>;
6
+ /**
7
+ * OpenAICachingCacheable is a wrapper around the Fetch API that adds caching capabilities for OpenAI requests.
8
+ * It uses a Cacheable instance to store and retrieve cached responses based on a hash of the request details.
9
+ */
10
+ export default class OpenAICache {
11
+ private readonly _cache;
12
+ private readonly _markResponseEnabled;
13
+ static readonly MarkResponseName = "X_FROM_OPENAI_CACHE";
14
+ /**
15
+ * Creates a new instance of OpenAICache.
16
+ *
17
+ * @param cache cacheable instance
18
+ * @param options.markResponseEnabled whether to mark cached responses with an additional property in the JSON body (default: true).
19
+ * This can be useful for downstream logic that needs to differentiate between live and cached responses, but it does modify
20
+ * the original response body so it is optional. so the response is { X_FROM_OPENAI_CACHE: true, ...originalResponseBody }
21
+ */
22
+ constructor(cache?: Cacheable, { markResponseEnabled }?: {
23
+ markResponseEnabled?: boolean;
24
+ });
25
+ /**
26
+ * Cleans the OpenAI cache by deleting all cached values.
27
+ */
28
+ cleanCache(): Promise<void>;
29
+ /**
30
+ * return a fetch function that can be passed to OpenAI client for caching support
31
+ *
32
+ * ```js
33
+ * const openai = new OpenAI({
34
+ * fetch: openaiCache.getFetchFn()
35
+ * });
36
+ * ```
37
+ */
38
+ getFetchFn(): (input: FetchInput, init?: FetchInit) => Promise<FetchResponse>;
39
+ /**
40
+ * This is the fetch() implementation that adds caching for OpenAI requests.
41
+ *
42
+ * @param input The resource that you wish to fetch.
43
+ * @param init An options object containing any custom settings that you want to apply to the request.
44
+ * @returns A Promise that resolves to the Response to that request.
45
+ */
46
+ private _fetch;
47
+ /**
48
+ * Remove transfer/content encodings that no longer apply once the body is materialized
49
+ * and optionally set a correct content-length for the cached payload.
50
+ */
51
+ private static _normalizeHeaders;
52
+ private static _serializeBodyForHash;
53
+ }
54
+ export {};
@@ -0,0 +1,157 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ // node imports
7
+ const node_crypto_1 = __importDefault(require("node:crypto"));
8
+ const node_buffer_1 = require("node:buffer");
9
+ const cacheable_1 = require("cacheable");
10
+ ///////////////////////////////////////////////////////////////////////////////
11
+ ///////////////////////////////////////////////////////////////////////////////
12
+ // OpenAICache
13
+ ///////////////////////////////////////////////////////////////////////////////
14
+ ///////////////////////////////////////////////////////////////////////////////
15
+ /**
16
+ * OpenAICachingCacheable is a wrapper around the Fetch API that adds caching capabilities for OpenAI requests.
17
+ * It uses a Cacheable instance to store and retrieve cached responses based on a hash of the request details.
18
+ */
19
+ class OpenAICache {
20
+ /**
21
+ * Creates a new instance of OpenAICache.
22
+ *
23
+ * @param cache cacheable instance
24
+ * @param options.markResponseEnabled whether to mark cached responses with an additional property in the JSON body (default: true).
25
+ * This can be useful for downstream logic that needs to differentiate between live and cached responses, but it does modify
26
+ * the original response body so it is optional. so the response is { X_FROM_OPENAI_CACHE: true, ...originalResponseBody }
27
+ */
28
+ constructor(cache, { markResponseEnabled = false } = {}) {
29
+ this._cache = cache !== null && cache !== void 0 ? cache : new cacheable_1.Cacheable();
30
+ this._markResponseEnabled = markResponseEnabled;
31
+ }
32
+ /**
33
+ * Cleans the OpenAI cache by deleting all cached values.
34
+ */
35
+ async cleanCache() {
36
+ await this._cache.clear();
37
+ }
38
+ /**
39
+ * return a fetch function that can be passed to OpenAI client for caching support
40
+ *
41
+ * ```js
42
+ * const openai = new OpenAI({
43
+ * fetch: openaiCache.getFetchFn()
44
+ * });
45
+ * ```
46
+ */
47
+ getFetchFn() {
48
+ return this._fetch.bind(this);
49
+ }
50
+ /**
51
+ * This is the fetch() implementation that adds caching for OpenAI requests.
52
+ *
53
+ * @param input The resource that you wish to fetch.
54
+ * @param init An options object containing any custom settings that you want to apply to the request.
55
+ * @returns A Promise that resolves to the Response to that request.
56
+ */
57
+ async _fetch(input, init) {
58
+ var _a, _b;
59
+ // Extract the URL from the input (string or Request)
60
+ const url = typeof input === "string" ? input : input instanceof Request ? input.url : input.toString();
61
+ // Normalize HTTP method
62
+ const method = ((init === null || init === void 0 ? void 0 : init.method) || "GET").toUpperCase();
63
+ // Generate body hash payload
64
+ const bodyForHash = OpenAICache._serializeBodyForHash(init === null || init === void 0 ? void 0 : init.body);
65
+ // If body type unsupported, skip caching
66
+ if (bodyForHash === null)
67
+ return fetch(input, init);
68
+ // Build cache key and file path
69
+ const cacheKey = node_crypto_1.default.createHash("sha256")
70
+ .update(`${method}:${url}:${bodyForHash}`)
71
+ .digest("hex");
72
+ const cached = (await this._cache.get(cacheKey));
73
+ if (cached !== undefined && process.env.OPENAI_CACHE !== "disabled") {
74
+ const bodyEncoding = (_a = cached.bodyEncoding) !== null && _a !== void 0 ? _a : "utf8";
75
+ const cachedBodyBuffer = node_buffer_1.Buffer.from(cached.body, bodyEncoding);
76
+ // Return cached response
77
+ let newResponse = new Response(cachedBodyBuffer, {
78
+ status: cached.status,
79
+ headers: cached.headers,
80
+ });
81
+ // honor this._markResponseEnabled option to indicate cache hit
82
+ const contentTypeIsJson = ((_b = newResponse.headers.get("content-type")) === null || _b === void 0 ? void 0 : _b.includes("application/json")) ? true : false;
83
+ if (this._markResponseEnabled && contentTypeIsJson) {
84
+ try {
85
+ // decode JSON from cachedBodyBuffer
86
+ const bodyJson = JSON.parse(cachedBodyBuffer.toString());
87
+ // Set the magic property to indicate this response is from cache
88
+ bodyJson.X_FROM_OPENAI_CACHE = true;
89
+ // Rebuild response with modified body
90
+ const modifiedBodyBuffer = node_buffer_1.Buffer.from(JSON.stringify(bodyJson));
91
+ newResponse = new Response(modifiedBodyBuffer, { status: cached.status, headers: cached.headers, });
92
+ }
93
+ catch (error) {
94
+ // If parsing fails, return the original cached response without modification
95
+ console.warn("Failed to parse cached response body as JSON for header modification:", error);
96
+ }
97
+ }
98
+ // Return cached response (body already buffered)
99
+ return newResponse;
100
+ }
101
+ // Perform network fetch
102
+ const response = await fetch(input, init);
103
+ const clonedResponse = response.clone();
104
+ // Materialize response body for caching
105
+ const responseBuffer = node_buffer_1.Buffer.from(await clonedResponse.arrayBuffer());
106
+ // Collect headers and normalize them
107
+ const headers = Array.from(clonedResponse.headers.entries());
108
+ const normalizedHeaders = OpenAICache._normalizeHeaders(headers, responseBuffer.length);
109
+ if (response.ok) {
110
+ await this._cache.set(cacheKey, {
111
+ status: clonedResponse.status,
112
+ headers: normalizedHeaders,
113
+ body: responseBuffer.toString("base64"),
114
+ bodyEncoding: "base64",
115
+ });
116
+ }
117
+ // Return live response (body already buffered)
118
+ return new Response(responseBuffer, { status: response.status, headers: normalizedHeaders });
119
+ }
120
+ ///////////////////////////////////////////////////////////////////////////////
121
+ ///////////////////////////////////////////////////////////////////////////////
122
+ // Private functions
123
+ ///////////////////////////////////////////////////////////////////////////////
124
+ ///////////////////////////////////////////////////////////////////////////////
125
+ /**
126
+ * Remove transfer/content encodings that no longer apply once the body is materialized
127
+ * and optionally set a correct content-length for the cached payload.
128
+ */
129
+ static _normalizeHeaders(headers, bodyLength) {
130
+ const drop = new Set([
131
+ "content-encoding", // body is already decoded by fetch()
132
+ "transfer-encoding",
133
+ "content-length", // will be recalculated
134
+ ]);
135
+ const filtered = headers.filter(([name]) => drop.has(name.toLowerCase()) === false);
136
+ if (bodyLength !== undefined) {
137
+ filtered.push(["content-length", String(bodyLength)]);
138
+ }
139
+ return filtered;
140
+ }
141
+ // Serialize body into a deterministic string for hashing
142
+ static _serializeBodyForHash(body) {
143
+ if (body === undefined || body === null)
144
+ return "";
145
+ if (typeof body === "string")
146
+ return body;
147
+ if (node_buffer_1.Buffer.isBuffer(body))
148
+ return body.toString("base64");
149
+ if (body instanceof ArrayBuffer)
150
+ return node_buffer_1.Buffer.from(body).toString("base64");
151
+ if (ArrayBuffer.isView(body))
152
+ return node_buffer_1.Buffer.from(body.buffer, body.byteOffset, body.byteLength).toString("base64");
153
+ return null; // unsupported body type
154
+ }
155
+ }
156
+ OpenAICache.MarkResponseName = "X_FROM_OPENAI_CACHE";
157
+ exports.default = OpenAICache;
@@ -6,10 +6,29 @@ type FetchResponse = Awaited<ReturnType<FetchFn>>;
6
6
  /**
7
7
  * OpenAICachingCacheable is a wrapper around the Fetch API that adds caching capabilities for OpenAI requests.
8
8
  * It uses a Cacheable instance to store and retrieve cached responses based on a hash of the request details.
9
+ * - **OPENAI_CACHE** environment variable can be set to "disabled" to disable cache and always fetch
10
+ * live responses (while still allowing manual cache management via cleanCache() and direct cache access)
11
+ *
12
+ * Example usage:
13
+ *
14
+ * ```js
15
+ * import { Cacheable } from 'cacheable';
16
+ * import OpenAICache from 'openai-cache';
17
+ * import KeyvSqlite from '@keyv/sqlite';
18
+ * import { OpenAI } from 'openai';
19
+ *
20
+ * const cache = new Cacheable({ secondary: new KeyvSqlite('sqlite://./openai_cache.sqlite') });
21
+ * const openaiCache = new OpenAICache(cache);
22
+ * const openaiClient = new OpenAI({
23
+ * fetch: openaiCache.getFetchFn()
24
+ * });
25
+ * ```
9
26
  */
10
27
  export default class OpenAICache {
11
28
  private readonly _cache;
12
29
  private readonly _markResponseEnabled;
30
+ private readonly _verboseLevel;
31
+ private disabledCacheWarningLogged;
13
32
  static readonly MarkResponseName = "X_FROM_OPENAI_CACHE";
14
33
  /**
15
34
  * Creates a new instance of OpenAICache.
@@ -19,8 +38,9 @@ export default class OpenAICache {
19
38
  * This can be useful for downstream logic that needs to differentiate between live and cached responses, but it does modify
20
39
  * the original response body so it is optional. so the response is { X_FROM_OPENAI_CACHE: true, ...originalResponseBody }
21
40
  */
22
- constructor(cache?: Cacheable, { markResponseEnabled }?: {
41
+ constructor(cache?: Cacheable, { markResponseEnabled, verboseLevel, }?: {
23
42
  markResponseEnabled?: boolean;
43
+ verboseLevel?: number;
24
44
  });
25
45
  /**
26
46
  * Cleans the OpenAI cache by deleting all cached values.
@@ -44,11 +64,18 @@ export default class OpenAICache {
44
64
  * @returns A Promise that resolves to the Response to that request.
45
65
  */
46
66
  private _fetch;
67
+ /**
68
+ * Wraps a streaming response in a pass-through ReadableStream that caches the
69
+ * full body in the background once the stream completes.
70
+ */
71
+ private static _createCachingStreamResponse;
47
72
  /**
48
73
  * Remove transfer/content encodings that no longer apply once the body is materialized
49
74
  * and optionally set a correct content-length for the cached payload.
50
75
  */
51
76
  private static _normalizeHeaders;
77
+ private static _isStreamingResponse;
52
78
  private static _serializeBodyForHash;
53
79
  }
54
80
  export {};
81
+ //# sourceMappingURL=openai_cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openai_cache.d.ts","sourceRoot":"","sources":["../src/openai_cache.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAUtC,KAAK,OAAO,GAAG,OAAO,UAAU,CAAC,KAAK,CAAC;AACvC,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;AACzC,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;AAExC,KAAK,aAAa,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC;AAelD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,OAAO,OAAO,WAAW;IAC/B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAY;IACnC,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAU;IAC/C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,0BAA0B,CAAkB;IACpD,gBAAuB,gBAAgB,yBAAyB;IAEhE;;;;;;;OAOG;gBACS,KAAK,CAAC,EAAE,SAAS,EAAE,EAC9B,mBAA2B,EAC3B,YAAgB,GAChB,GAAE;QACF,mBAAmB,CAAC,EAAE,OAAO,CAAC;QAC9B,YAAY,CAAC,EAAE,MAAM,CAAA;KAChB;IAMN;;OAEG;IACU,UAAU;IAIvB;;;;;;;;OAQG;IACI,UAAU,IAAI,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,CAAC,EAAE,SAAS,KAAK,OAAO,CAAC,aAAa,CAAC;IAIpF;;;;;;OAMG;YACW,MAAM;IA6HpB;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,4BAA4B;IAwC3C;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,iBAAiB;IAehC,OAAO,CAAC,MAAM,CAAC,oBAAoB;IASnC,OAAO,CAAC,MAAM,CAAC,qBAAqB;CAmBpC"}
@@ -7,6 +7,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
7
7
  const node_crypto_1 = __importDefault(require("node:crypto"));
8
8
  const node_buffer_1 = require("node:buffer");
9
9
  const cacheable_1 = require("cacheable");
10
+ const chalk_1 = __importDefault(require("chalk"));
10
11
  ///////////////////////////////////////////////////////////////////////////////
11
12
  ///////////////////////////////////////////////////////////////////////////////
12
13
  // OpenAICache
@@ -15,8 +16,30 @@ const cacheable_1 = require("cacheable");
15
16
  /**
16
17
  * OpenAICachingCacheable is a wrapper around the Fetch API that adds caching capabilities for OpenAI requests.
17
18
  * It uses a Cacheable instance to store and retrieve cached responses based on a hash of the request details.
19
+ * - **OPENAI_CACHE** environment variable can be set to "disabled" to disable cache and always fetch
20
+ * live responses (while still allowing manual cache management via cleanCache() and direct cache access)
21
+ *
22
+ * Example usage:
23
+ *
24
+ * ```js
25
+ * import { Cacheable } from 'cacheable';
26
+ * import OpenAICache from 'openai-cache';
27
+ * import KeyvSqlite from '@keyv/sqlite';
28
+ * import { OpenAI } from 'openai';
29
+ *
30
+ * const cache = new Cacheable({ secondary: new KeyvSqlite('sqlite://./openai_cache.sqlite') });
31
+ * const openaiCache = new OpenAICache(cache);
32
+ * const openaiClient = new OpenAI({
33
+ * fetch: openaiCache.getFetchFn()
34
+ * });
35
+ * ```
18
36
  */
19
37
  class OpenAICache {
38
+ _cache;
39
+ _markResponseEnabled;
40
+ _verboseLevel;
41
+ disabledCacheWarningLogged = false;
42
+ static MarkResponseName = "X_FROM_OPENAI_CACHE";
20
43
  /**
21
44
  * Creates a new instance of OpenAICache.
22
45
  *
@@ -25,9 +48,10 @@ class OpenAICache {
25
48
  * This can be useful for downstream logic that needs to differentiate between live and cached responses, but it does modify
26
49
  * the original response body so it is optional. so the response is { X_FROM_OPENAI_CACHE: true, ...originalResponseBody }
27
50
  */
28
- constructor(cache, { markResponseEnabled = false } = {}) {
29
- this._cache = cache !== null && cache !== void 0 ? cache : new cacheable_1.Cacheable();
51
+ constructor(cache, { markResponseEnabled = false, verboseLevel = 0, } = {}) {
52
+ this._cache = cache ?? new cacheable_1.Cacheable();
30
53
  this._markResponseEnabled = markResponseEnabled;
54
+ this._verboseLevel = verboseLevel;
31
55
  }
32
56
  /**
33
57
  * Cleans the OpenAI cache by deleting all cached values.
@@ -55,31 +79,56 @@ class OpenAICache {
55
79
  * @returns A Promise that resolves to the Response to that request.
56
80
  */
57
81
  async _fetch(input, init) {
58
- var _a, _b;
59
82
  // Extract the URL from the input (string or Request)
60
83
  const url = typeof input === "string" ? input : input instanceof Request ? input.url : input.toString();
61
84
  // Normalize HTTP method
62
- const method = ((init === null || init === void 0 ? void 0 : init.method) || "GET").toUpperCase();
85
+ const method = (init?.method || "GET").toUpperCase();
86
+ // debugger
63
87
  // Generate body hash payload
64
- const bodyForHash = OpenAICache._serializeBodyForHash(init === null || init === void 0 ? void 0 : init.body);
88
+ const bodyForHash = OpenAICache._serializeBodyForHash(init?.body);
65
89
  // If body type unsupported, skip caching
66
- if (bodyForHash === null)
90
+ if (bodyForHash === null) {
91
+ if (this._verboseLevel > 1) {
92
+ console.warn(chalk_1.default.yellow(`Skipping cache for ${method} ${url} due to unsupported body type`));
93
+ }
67
94
  return fetch(input, init);
95
+ }
68
96
  // Build cache key and file path
69
97
  const cacheKey = node_crypto_1.default.createHash("sha256")
70
98
  .update(`${method}:${url}:${bodyForHash}`)
71
99
  .digest("hex");
72
- const cached = (await this._cache.get(cacheKey));
73
- if (cached !== undefined && process.env.OPENAI_CACHE !== "disabled") {
74
- const bodyEncoding = (_a = cached.bodyEncoding) !== null && _a !== void 0 ? _a : "utf8";
75
- const cachedBodyBuffer = node_buffer_1.Buffer.from(cached.body, bodyEncoding);
100
+ // console.log("cacheKey", cacheKey, bodyForHash);
101
+ // Log a warning if cache is disabled via environment variable, but only once to avoid spamming the console
102
+ if (process.env.OPENAI_CACHE === "disabled" && this.disabledCacheWarningLogged === false) {
103
+ if (this._verboseLevel > 0) {
104
+ console.warn(chalk_1.default.red("OpenAI cache is disabled via OPENAI_CACHE=disabled. All requests will be live and no caching will occur."));
105
+ }
106
+ this.disabledCacheWarningLogged = true;
107
+ }
108
+ const cachedValue = await this._cache.get(cacheKey);
109
+ if (cachedValue !== undefined && process.env.OPENAI_CACHE !== "disabled") {
110
+ const bodyEncoding = cachedValue.bodyEncoding ?? "utf8";
111
+ const cachedBodyBuffer = node_buffer_1.Buffer.from(cachedValue.body, bodyEncoding);
112
+ // For streaming SSE responses, return directly without JSON modification
113
+ if (OpenAICache._isStreamingResponse(cachedValue.headers)) {
114
+ if (this._verboseLevel > 1) {
115
+ console.log(chalk_1.default.green(`Cache hit for streamed ${method} ${url}`));
116
+ }
117
+ return new Response(cachedBodyBuffer, {
118
+ status: cachedValue.status,
119
+ headers: cachedValue.headers,
120
+ });
121
+ }
122
+ if (this._verboseLevel > 1) {
123
+ console.log(chalk_1.default.green(`Cache hit for non-streamed ${method} ${url}`));
124
+ }
76
125
  // Return cached response
77
126
  let newResponse = new Response(cachedBodyBuffer, {
78
- status: cached.status,
79
- headers: cached.headers,
127
+ status: cachedValue.status,
128
+ headers: cachedValue.headers,
80
129
  });
81
130
  // honor this._markResponseEnabled option to indicate cache hit
82
- const contentTypeIsJson = ((_b = newResponse.headers.get("content-type")) === null || _b === void 0 ? void 0 : _b.includes("application/json")) ? true : false;
131
+ const contentTypeIsJson = newResponse.headers.get("content-type")?.includes("application/json") ? true : false;
83
132
  if (this._markResponseEnabled && contentTypeIsJson) {
84
133
  try {
85
134
  // decode JSON from cachedBodyBuffer
@@ -88,7 +137,7 @@ class OpenAICache {
88
137
  bodyJson.X_FROM_OPENAI_CACHE = true;
89
138
  // Rebuild response with modified body
90
139
  const modifiedBodyBuffer = node_buffer_1.Buffer.from(JSON.stringify(bodyJson));
91
- newResponse = new Response(modifiedBodyBuffer, { status: cached.status, headers: cached.headers, });
140
+ newResponse = new Response(modifiedBodyBuffer, { status: cachedValue.status, headers: cachedValue.headers, });
92
141
  }
93
142
  catch (error) {
94
143
  // If parsing fails, return the original cached response without modification
@@ -100,6 +149,19 @@ class OpenAICache {
100
149
  }
101
150
  // Perform network fetch
102
151
  const response = await fetch(input, init);
152
+ // For streaming SSE responses, pipe through to enable progressive streaming + background caching
153
+ if (OpenAICache._isStreamingResponse(response.headers)) {
154
+ if (this._verboseLevel > 1) {
155
+ console.log(chalk_1.default.yellow(`Cache miss for streamed ${method} ${url} - caching in background as it streams in`));
156
+ }
157
+ if (!response.ok || !response.body) {
158
+ return response;
159
+ }
160
+ return OpenAICache._createCachingStreamResponse(response, this._cache, cacheKey);
161
+ }
162
+ if (this._verboseLevel > 1) {
163
+ console.log(chalk_1.default.yellow(`Cache miss for non-streamed ${method} ${url} - caching in background`));
164
+ }
103
165
  const clonedResponse = response.clone();
104
166
  // Materialize response body for caching
105
167
  const responseBuffer = node_buffer_1.Buffer.from(await clonedResponse.arrayBuffer());
@@ -122,6 +184,42 @@ class OpenAICache {
122
184
  // Private functions
123
185
  ///////////////////////////////////////////////////////////////////////////////
124
186
  ///////////////////////////////////////////////////////////////////////////////
187
+ /**
188
+ * Wraps a streaming response in a pass-through ReadableStream that caches the
189
+ * full body in the background once the stream completes.
190
+ */
191
+ static _createCachingStreamResponse(response, cache, cacheKey) {
192
+ const responseStatus = response.status;
193
+ const responseHeaders = Array.from(response.headers.entries());
194
+ const chunks = [];
195
+ const reader = response.body.getReader();
196
+ const passThrough = new ReadableStream({
197
+ async pull(controller) {
198
+ const { done, value } = await reader.read();
199
+ if (done) {
200
+ controller.close();
201
+ const fullBody = node_buffer_1.Buffer.concat(chunks);
202
+ const normalizedHeaders = OpenAICache._normalizeHeaders(responseHeaders, fullBody.length);
203
+ cache.set(cacheKey, {
204
+ status: responseStatus,
205
+ headers: normalizedHeaders,
206
+ body: fullBody.toString("base64"),
207
+ bodyEncoding: "base64",
208
+ }).catch(() => { });
209
+ return;
210
+ }
211
+ chunks.push(value);
212
+ controller.enqueue(value);
213
+ },
214
+ cancel() {
215
+ reader.cancel();
216
+ },
217
+ });
218
+ return new Response(passThrough, {
219
+ status: responseStatus,
220
+ headers: responseHeaders,
221
+ });
222
+ }
125
223
  /**
126
224
  * Remove transfer/content encodings that no longer apply once the body is materialized
127
225
  * and optionally set a correct content-length for the cached payload.
@@ -138,20 +236,34 @@ class OpenAICache {
138
236
  }
139
237
  return filtered;
140
238
  }
239
+ // Detect streaming SSE responses by content-type header
240
+ static _isStreamingResponse(headers) {
241
+ if (headers instanceof Headers) {
242
+ return headers.get("content-type")?.includes("text/event-stream") ?? false;
243
+ }
244
+ const ct = headers.find(([name]) => name.toLowerCase() === "content-type");
245
+ return ct?.[1]?.includes("text/event-stream") ?? false;
246
+ }
141
247
  // Serialize body into a deterministic string for hashing
142
248
  static _serializeBodyForHash(body) {
249
+ // Handle null or undefined body
143
250
  if (body === undefined || body === null)
144
251
  return "";
252
+ // Handle string body
145
253
  if (typeof body === "string")
146
254
  return body;
255
+ // Handle Buffer body
147
256
  if (node_buffer_1.Buffer.isBuffer(body))
148
257
  return body.toString("base64");
258
+ // Handle ArrayBuffer body
149
259
  if (body instanceof ArrayBuffer)
150
260
  return node_buffer_1.Buffer.from(body).toString("base64");
261
+ // Handle typed arrays (Uint8Array, Int8Array, etc.)
151
262
  if (ArrayBuffer.isView(body))
152
263
  return node_buffer_1.Buffer.from(body.buffer, body.byteOffset, body.byteLength).toString("base64");
153
- return null; // unsupported body type
264
+ // Return null for unsupported body types to skip caching
265
+ return null;
154
266
  }
155
267
  }
156
- OpenAICache.MarkResponseName = "X_FROM_OPENAI_CACHE";
157
268
  exports.default = OpenAICache;
269
+ //# sourceMappingURL=openai_cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openai_cache.js","sourceRoot":"","sources":["../src/openai_cache.ts"],"names":[],"mappings":";;;;;AAAA,eAAe;AACf,8DAAiC;AACjC,6CAAqC;AACrC,yCAAsC;AACtC,kDAA0B;AAsB1B,+EAA+E;AAC/E,+EAA+E;AAC/E,cAAc;AACd,+EAA+E;AAC/E,+EAA+E;AAE/E;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAqB,WAAW;IACd,MAAM,CAAY;IAClB,oBAAoB,CAAU;IAC9B,aAAa,CAAS;IAC/B,0BAA0B,GAAY,KAAK,CAAC;IAC7C,MAAM,CAAU,gBAAgB,GAAG,qBAAqB,CAAC;IAEhE;;;;;;;OAOG;IACH,YAAY,KAAiB,EAAE,EAC9B,mBAAmB,GAAG,KAAK,EAC3B,YAAY,GAAG,CAAC,MAIb,EAAE;QACL,IAAI,CAAC,MAAM,GAAG,KAAK,IAAI,IAAI,qBAAS,EAAE,CAAC;QACvC,IAAI,CAAC,oBAAoB,GAAG,mBAAmB,CAAC;QAChD,IAAI,CAAC,aAAa,GAAG,YAAY,CAAC;IACnC,CAAC;IAED;;OAEG;IACI,KAAK,CAAC,UAAU;QACtB,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;IAC3B,CAAC;IAED;;;;;;;;OAQG;IACI,UAAU;QAChB,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC/B,CAAC;IAED;;;;;;OAMG;IACK,KAAK,CAAC,MAAM,CAAC,KAAiB,EAAE,IAAgB;QACvD,qDAAqD;QACrD,MAAM,GAAG,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,YAAY,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;QACxG,wBAAwB;QACxB,MAAM,MAAM,GAAG,CAAC,IAAI,EAAE,MAAM,IAAI,KAAK,CAAC,CAAC,WAAW,EAAE,CAAC;QAErD,WAAW;QAEX,6BAA6B;QAC7B,MAAM,WAAW,GAAG,WAAW,CAAC,qBAAqB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAClE,yCAAyC;QACzC,IAAI,WAAW,KAAK,IAAI,EAAE,CAAC;YAC1B,IAAI,IAAI,CAAC,aAAa,GAAG,CAAC,EAAE,CAAC;gBAC5B,OAAO,CAAC,IAAI,CAAC,eAAK,CAAC,MAAM,CAAC,sBAAsB,MAAM,IAAI,GAAG,+BAA+B,CAAC,CAAC,CAAC;YAChG,CAAC;YACD,OAAO,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QAC3B,CAAC;QAID,gCAAgC;QAChC,MAAM,QAAQ,GAAG,qBAAM,CAAC,UAAU,CAAC,QAAQ,CAAC;aAC1C,MAAM,CAAC,GAAG,MAAM,IAAI,GAAG,IAAI,WAAW,EAAE,CAAC;aACzC,MAAM,CAAC,KAAK,CAAC,CAAC;QAEhB,kDAAkD;QAElD,2GAA2G;QAC3G,IAAI,OAAO,CAAC,GAAG,CAAC,YAAY,KAAK,UAAU,IAAI,IAAI,CAAC,0BAA0B,KAAK,KAAK,EAAE,CAAC;YAC1F,IAAI,IAAI,CAAC,aAAa,GAAG,CAAC,EAAE,CAAC;gBAC5B,OAAO,CAAC,IAAI,CAAC,eAAK,CAAC,GAAG,CAAC,0GAA0G,CAAC,CAAC,CAAC;YACrI,CAAC;YACD,IAAI,CAAC,0BAA0B,GAAG,IAAI,CAAC;QACxC,CAAC;QAED,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,CAAsB,QAAQ,CAAC,CAAA;QACxE,IAAI,WAAW,KAAK,SAAS,IAAI,OAAO,CAAC,GAAG,CAAC,YAAY,KAAK,UAAU,EAAE,CAAC;YAC1E,MAAM,YAAY,GAAmB,WAAW,CAAC,YAAY,IAAI,MAAM,CAAC;YACxE,MAAM,gBAAgB,GAAG,oBAAM,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC;YAGrE,yEAAyE;YACzE,IAAI,WAAW,CAAC,oBAAoB,CAAC,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC3D,IAAI,IAAI,CAAC,aAAa,GAAG,CAAC,EAAE,CAAC;oBAC5B,OAAO,CAAC,GAAG,CAAC,eAAK,CAAC,KAAK,CAAC,0BAA0B,MAAM,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC;gBACrE,CAAC;gBACD,OAAO,IAAI,QAAQ,CAAC,gBAAgB,EAAE;oBACrC,MAAM,EAAE,WAAW,CAAC,MAAM;oBAC1B,OAAO,EAAE,WAAW,CAAC,OAAO;iBAC5B,CAAC,CAAC;YACJ,CAAC;YAED,IAAI,IAAI,CAAC,aAAa,GAAG,CAAC,EAAE,CAAC;gBAC5B,OAAO,CAAC,GAAG,CAAC,eAAK,CAAC,KAAK,CAAC,8BAA8B,MAAM,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC;YACzE,CAAC;YAED,yBAAyB;YACzB,IAAI,WAAW,GAAG,IAAI,QAAQ,CAAC,gBAAgB,EAAE;gBAChD,MAAM,EAAE,WAAW,CAAC,MAAM;gBAC1B,OAAO,EAAE,WAAW,CAAC,OAAO;aAC5B,CAAC,CAAC;YACH,+DAA+D;YAC/D,MAAM,iBAAiB,GAAG,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,EAAE,QAAQ,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC;YAC/G,IAAI,IAAI,CAAC,oBAAoB,IAAI,iBAAiB,EAAE,CAAC;gBACpD,IAAI,CAAC;oBACJ,oCAAoC;oBACpC,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,QAAQ,EAAE,CAAC,CAAC;oBACzD,iEAAiE;oBACjE,QAAQ,CAAC,mBAAmB,GAAG,IAAI,CAAC;oBACpC,sCAAsC;oBACtC,MAAM,kBAAkB,GAAG,oBAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC;oBACjE,WAAW,GAAG,IAAI,QAAQ,CAAC,kBAAkB,EAAE,EAAE,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,OAAO,EAAE,WAAW,CAAC,OAAO,GAAG,CAAC,CAAC;gBAC/G,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBAChB,6EAA6E;oBAC7E,OAAO,CAAC,IAAI,CAAC,uEAAuE,EAAE,KAAK,CAAC,CAAC;gBAC9F,CAAC;YACF,CAAC;YACD,iDAAiD;YACjD,OAAO,WAAW,CAAC;QACpB,CAAC;QAED,wBAAwB;QACxB,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QAE1C,iGAAiG;QACjG,IAAI,WAAW,CAAC,oBAAoB,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;YACxD,IAAI,IAAI,CAAC,aAAa,GAAG,CAAC,EAAE,CAAC;gBAC5B,OAAO,CAAC,GAAG,CAAC,eAAK,CAAC,MAAM,CAAC,2BAA2B,MAAM,IAAI,GAAG,2CAA2C,CAAC,CAAC,CAAC;YAChH,CAAC;YACD,IAAI,CAAC,QAAQ,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;gBACpC,OAAO,QAAQ,CAAC;YACjB,CAAC;YACD,OAAO,WAAW,CAAC,4BAA4B,CAAC,QAAQ,EAAE,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QAClF,CAAC;QAED,IAAI,IAAI,CAAC,aAAa,GAAG,CAAC,EAAE,CAAC;YAC5B,OAAO,CAAC,GAAG,CAAC,eAAK,CAAC,MAAM,CAAC,+BAA+B,MAAM,IAAI,GAAG,0BAA0B,CAAC,CAAC,CAAC;QACnG,CAAC;QAED,MAAM,cAAc,GAAG,QAAQ,CAAC,KAAK,EAAE,CAAC;QACxC,wCAAwC;QACxC,MAAM,cAAc,GAAG,oBAAM,CAAC,IAAI,CAAC,MAAM,cAAc,CAAC,WAAW,EAAE,CAAC,CAAC;QACvE,qCAAqC;QACrC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;QAC7D,MAAM,iBAAiB,GAAG,WAAW,CAAC,iBAAiB,CAAC,OAAO,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;QAExF,IAAI,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE;gBAC/B,MAAM,EAAE,cAAc,CAAC,MAAM;gBAC7B,OAAO,EAAE,iBAAiB;gBAC1B,IAAI,EAAE,cAAc,CAAC,QAAQ,CAAC,QAAQ,CAAC;gBACvC,YAAY,EAAE,QAAQ;aACtB,CAAC,CAAC;QACJ,CAAC;QAED,+CAA+C;QAC/C,OAAO,IAAI,QAAQ,CAAC,cAAc,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,OAAO,EAAE,iBAAiB,EAAE,CAAC,CAAC;IAC9F,CAAC;IAED,+EAA+E;IAC/E,+EAA+E;IAC/E,oBAAoB;IACpB,+EAA+E;IAC/E,+EAA+E;IAE/E;;;OAGG;IACK,MAAM,CAAC,4BAA4B,CAC1C,QAAuB,EACvB,KAAgB,EAChB,QAAgB;QAEhB,MAAM,cAAc,GAAG,QAAQ,CAAC,MAAM,CAAC;QACvC,MAAM,eAAe,GAAG,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;QAC/D,MAAM,MAAM,GAAiB,EAAE,CAAC;QAChC,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAK,CAAC,SAAS,EAAE,CAAC;QAE1C,MAAM,WAAW,GAAG,IAAI,cAAc,CAAa;YAClD,KAAK,CAAC,IAAI,CAAC,UAAU;gBACpB,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;gBAC5C,IAAI,IAAI,EAAE,CAAC;oBACV,UAAU,CAAC,KAAK,EAAE,CAAC;oBACnB,MAAM,QAAQ,GAAG,oBAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;oBACvC,MAAM,iBAAiB,GAAG,WAAW,CAAC,iBAAiB,CAAC,eAAe,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;oBAC1F,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE;wBACnB,MAAM,EAAE,cAAc;wBACtB,OAAO,EAAE,iBAAiB;wBAC1B,IAAI,EAAE,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC;wBACjC,YAAY,EAAE,QAAQ;qBACtB,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;oBACpB,OAAO;gBACR,CAAC;gBACD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBACnB,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YAC3B,CAAC;YACD,MAAM;gBACL,MAAM,CAAC,MAAM,EAAE,CAAC;YACjB,CAAC;SACD,CAAC,CAAC;QAEH,OAAO,IAAI,QAAQ,CAAC,WAAW,EAAE;YAChC,MAAM,EAAE,cAAc;YACtB,OAAO,EAAE,eAAe;SACxB,CAAC,CAAC;IACJ,CAAC;IAGD;;;OAGG;IACK,MAAM,CAAC,iBAAiB,CAAC,OAA2B,EAAE,UAAmB;QAChF,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC;YACpB,kBAAkB,EAAE,qCAAqC;YACzD,mBAAmB;YACnB,gBAAgB,EAAE,uBAAuB;SACzC,CAAC,CAAC;QAEH,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,KAAK,KAAK,CAAC,CAAC;QACpF,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;YAC9B,QAAQ,CAAC,IAAI,CAAC,CAAC,gBAAgB,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QACvD,CAAC;QACD,OAAO,QAAQ,CAAC;IACjB,CAAC;IAED,wDAAwD;IAChD,MAAM,CAAC,oBAAoB,CAAC,OAAqC;QACxE,IAAI,OAAO,YAAY,OAAO,EAAE,CAAC;YAChC,OAAO,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,EAAE,QAAQ,CAAC,mBAAmB,CAAC,IAAI,KAAK,CAAC;QAC5E,CAAC;QACD,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,cAAc,CAAC,CAAC;QAC3E,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,mBAAmB,CAAC,IAAI,KAAK,CAAC;IACxD,CAAC;IAED,yDAAyD;IACjD,MAAM,CAAC,qBAAqB,CAAC,IAAsC;QAC1E,gCAAgC;QAChC,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,IAAI;YAAE,OAAO,EAAE,CAAC;QAEnD,qBAAqB;QACrB,IAAI,OAAO,IAAI,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QAE1C,qBAAqB;QACrB,IAAI,oBAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAE1D,0BAA0B;QAC1B,IAAI,IAAI,YAAY,WAAW;YAAE,OAAO,oBAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAE7E,oDAAoD;QACpD,IAAI,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC;YAAE,OAAO,oBAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAEnH,yDAAyD;QACzD,OAAO,IAAI,CAAC;IACb,CAAC;;AA7QF,8BA8QC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openai-cache",
3
- "version": "1.0.14",
3
+ "version": "1.0.22",
4
4
  "main": "dist/openai_cache.js",
5
5
  "types": "dist/openai_cache.d.ts",
6
6
  "exports": {
@@ -25,7 +25,7 @@
25
25
  "prepublishOnly": "npm run test && npm run build",
26
26
  "test": "tsx --test ./test/*_test.ts",
27
27
  "test:watch": "tsx --test --watch ./test/*_test.ts",
28
- "publish:all": "npm run build && npm run version:patch && npm publish --access public",
28
+ "publish:all": "npm run build && npm run version:patch && git add . && git commit -m 'version bump' && npm publish --access public",
29
29
  "version:patch": "npm version patch",
30
30
  "version:minor": "npm version minor",
31
31
  "version:major": "npm version major",
@@ -56,15 +56,17 @@
56
56
  },
57
57
  "devDependencies": {
58
58
  "@keyv/sqlite": "^4.0.8",
59
- "@types/node": "^25.1.0",
59
+ "@openai/agents": "^0.8.1",
60
+ "@types/node": "^25.5.2",
60
61
  "keyv": "^5.6.0",
61
62
  "openai": "^6.17.0",
62
63
  "ts-node": "^10.9.2",
63
64
  "tsx": "^4.21.0",
64
- "typescript": "^5.9.3",
65
+ "typescript": "^6.0.2",
65
66
  "zod": "^4.3.6"
66
67
  },
67
68
  "dependencies": {
68
- "cacheable": "^2.3.3"
69
+ "cacheable": "^2.3.3",
70
+ "chalk": "^5.6.2"
69
71
  }
70
- }
72
+ }