openai-sqlite-cache 0.0.1__py3-none-any.whl
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.
- cached_openai/__init__.py +175 -0
- cached_openai/_cache.py +145 -0
- cached_openai/_client.py +166 -0
- cached_openai/_fingerprint.py +186 -0
- cached_openai/_http.py +515 -0
- cached_openai/_module_alias.py +86 -0
- cached_openai/_settings.py +122 -0
- cached_openai/_version.py +3 -0
- cached_openai/py.typed +1 -0
- openai_sqlite_cache-0.0.1.dist-info/METADATA +151 -0
- openai_sqlite_cache-0.0.1.dist-info/RECORD +14 -0
- openai_sqlite_cache-0.0.1.dist-info/WHEEL +5 -0
- openai_sqlite_cache-0.0.1.dist-info/licenses/LICENSE +21 -0
- openai_sqlite_cache-0.0.1.dist-info/top_level.txt +1 -0
cached_openai/_http.py
ADDED
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
"""Caching HTTP-client adapters used by the OpenAI SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import importlib
|
|
7
|
+
import threading
|
|
8
|
+
import warnings
|
|
9
|
+
import weakref
|
|
10
|
+
import zlib
|
|
11
|
+
from typing import Any, Awaitable, Callable, Iterable, List, Optional, Tuple
|
|
12
|
+
|
|
13
|
+
from ._cache import CachedResponse, SQLiteCache
|
|
14
|
+
from ._fingerprint import request_fingerprint
|
|
15
|
+
from ._settings import CacheSettings
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _sdk_http_module() -> Any:
|
|
19
|
+
"""Resolve the HTTP module used by this installed OpenAI SDK.
|
|
20
|
+
|
|
21
|
+
OpenAI 1.x/early 2.x use ``httpx``. Newer SDK releases can use the
|
|
22
|
+
API-compatible ``httpx2`` package, so importing the SDK's own dependency is
|
|
23
|
+
more robust than pinning this package to either implementation.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
base_client = importlib.import_module("openai._base_client")
|
|
27
|
+
for name in ("httpx2", "httpx"):
|
|
28
|
+
module = getattr(base_client, name, None)
|
|
29
|
+
if module is not None:
|
|
30
|
+
return module
|
|
31
|
+
for name in ("httpx2", "httpx"):
|
|
32
|
+
try:
|
|
33
|
+
return importlib.import_module(name)
|
|
34
|
+
except ImportError:
|
|
35
|
+
continue
|
|
36
|
+
raise ImportError(
|
|
37
|
+
"openai-sqlite-cache could not locate the HTTP library used by openai"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
_http = _sdk_http_module()
|
|
42
|
+
_SYNC_LOCKS = [threading.Lock() for _ in range(127)]
|
|
43
|
+
_ASYNC_LOCKS: "weakref.WeakKeyDictionary[Any, List[asyncio.Lock]]" = (
|
|
44
|
+
weakref.WeakKeyDictionary()
|
|
45
|
+
)
|
|
46
|
+
_ASYNC_LOCKS_GUARD = threading.Lock()
|
|
47
|
+
_warning_lock = threading.Lock()
|
|
48
|
+
_warned_messages = set()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _warn_once(message: str) -> None:
|
|
52
|
+
with _warning_lock:
|
|
53
|
+
if message in _warned_messages:
|
|
54
|
+
return
|
|
55
|
+
_warned_messages.add(message)
|
|
56
|
+
warnings.warn(message, RuntimeWarning, stacklevel=3)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _sync_lock(cache_key: str) -> Any:
|
|
60
|
+
return _SYNC_LOCKS[int(cache_key[-8:], 16) % len(_SYNC_LOCKS)]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _async_lock(cache_key: str) -> asyncio.Lock:
|
|
64
|
+
loop = asyncio.get_running_loop()
|
|
65
|
+
with _ASYNC_LOCKS_GUARD:
|
|
66
|
+
locks = _ASYNC_LOCKS.get(loop)
|
|
67
|
+
if locks is None:
|
|
68
|
+
locks = [asyncio.Lock() for _ in range(127)]
|
|
69
|
+
_ASYNC_LOCKS[loop] = locks
|
|
70
|
+
return locks[int(cache_key[-8:], 16) % len(locks)]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
_DROPPED_RESPONSE_HEADERS = {
|
|
74
|
+
"connection",
|
|
75
|
+
"content-encoding",
|
|
76
|
+
"content-length",
|
|
77
|
+
"keep-alive",
|
|
78
|
+
"proxy-authenticate",
|
|
79
|
+
"proxy-authorization",
|
|
80
|
+
"te",
|
|
81
|
+
"trailer",
|
|
82
|
+
"transfer-encoding",
|
|
83
|
+
"upgrade",
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _storable_headers(headers: Any, *, raw_body: bool) -> List[Tuple[str, str]]:
|
|
88
|
+
items = (
|
|
89
|
+
headers.multi_items() if hasattr(headers, "multi_items") else headers.items()
|
|
90
|
+
)
|
|
91
|
+
return [
|
|
92
|
+
(str(name), str(value))
|
|
93
|
+
for name, value in items
|
|
94
|
+
if str(name).lower() not in _DROPPED_RESPONSE_HEADERS
|
|
95
|
+
or (raw_body and str(name).lower() == "content-encoding")
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class _SSETerminalDetector:
|
|
100
|
+
"""Find an SSE done frame incrementally, including gzip/deflate streams."""
|
|
101
|
+
|
|
102
|
+
_markers = (b"data: [DONE]", b"data:[DONE]")
|
|
103
|
+
|
|
104
|
+
def __init__(self, content_encoding: str):
|
|
105
|
+
self.complete = False
|
|
106
|
+
self._tail = b""
|
|
107
|
+
self._encoding = content_encoding.strip().lower()
|
|
108
|
+
self._decoder: Any = None
|
|
109
|
+
if self._encoding in {"gzip", "x-gzip"}:
|
|
110
|
+
self._decoder = zlib.decompressobj(zlib.MAX_WBITS | 16)
|
|
111
|
+
elif self._encoding == "deflate":
|
|
112
|
+
self._decoder = zlib.decompressobj()
|
|
113
|
+
|
|
114
|
+
def feed(self, chunk: bytes) -> bool:
|
|
115
|
+
if self.complete:
|
|
116
|
+
return True
|
|
117
|
+
if self._encoding and self._decoder is None:
|
|
118
|
+
return False
|
|
119
|
+
decoded = chunk
|
|
120
|
+
if self._decoder is not None:
|
|
121
|
+
try:
|
|
122
|
+
decoded = self._decoder.decompress(chunk)
|
|
123
|
+
except zlib.error:
|
|
124
|
+
return False
|
|
125
|
+
candidate = self._tail + decoded
|
|
126
|
+
self.complete = any(marker in candidate for marker in self._markers)
|
|
127
|
+
longest_marker = max(len(marker) for marker in self._markers)
|
|
128
|
+
self._tail = candidate[-(longest_marker - 1) :]
|
|
129
|
+
return self.complete
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class _CacheAccess:
|
|
133
|
+
def __init__(self, settings: CacheSettings):
|
|
134
|
+
self.settings = settings
|
|
135
|
+
self.backend: Optional[SQLiteCache]
|
|
136
|
+
try:
|
|
137
|
+
self.backend = SQLiteCache(settings.path)
|
|
138
|
+
except Exception as error:
|
|
139
|
+
self.backend = None
|
|
140
|
+
_warn_once(
|
|
141
|
+
"openai-sqlite-cache cache could not be opened; "
|
|
142
|
+
f"caching is disabled: {error}"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def get(self, key: str) -> Optional[CachedResponse]:
|
|
146
|
+
if self.backend is None:
|
|
147
|
+
return None
|
|
148
|
+
try:
|
|
149
|
+
return self.backend.get(key, self.settings.ttl_seconds)
|
|
150
|
+
except Exception as error:
|
|
151
|
+
_warn_once(
|
|
152
|
+
"openai-sqlite-cache cache read failed; "
|
|
153
|
+
f"requesting upstream instead: {error}"
|
|
154
|
+
)
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
def put(
|
|
158
|
+
self, key: str, response: Any, body: bytes, *, raw_body: bool = False
|
|
159
|
+
) -> None:
|
|
160
|
+
if self.backend is None or not 200 <= int(response.status_code) < 300:
|
|
161
|
+
return
|
|
162
|
+
try:
|
|
163
|
+
self.backend.put(
|
|
164
|
+
key,
|
|
165
|
+
int(response.status_code),
|
|
166
|
+
_storable_headers(response.headers, raw_body=raw_body),
|
|
167
|
+
body,
|
|
168
|
+
)
|
|
169
|
+
except Exception as error:
|
|
170
|
+
_warn_once(
|
|
171
|
+
"openai-sqlite-cache cache write failed; "
|
|
172
|
+
f"response was not cached: {error}"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
@staticmethod
|
|
176
|
+
def response(record: CachedResponse, request: Any) -> Any:
|
|
177
|
+
return _http.Response(
|
|
178
|
+
status_code=record.status_code,
|
|
179
|
+
headers=record.headers,
|
|
180
|
+
content=record.body,
|
|
181
|
+
request=request,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class _CachingSyncStream(_http.SyncByteStream):
|
|
186
|
+
def __init__(
|
|
187
|
+
self,
|
|
188
|
+
stream: Any,
|
|
189
|
+
on_complete: Callable[[bytes], None],
|
|
190
|
+
release: Callable[[], None],
|
|
191
|
+
content_encoding: str,
|
|
192
|
+
):
|
|
193
|
+
self._stream = stream
|
|
194
|
+
self._on_complete = on_complete
|
|
195
|
+
self._release = release
|
|
196
|
+
self._content_encoding = content_encoding
|
|
197
|
+
self._terminal = _SSETerminalDetector(content_encoding)
|
|
198
|
+
self._chunks = bytearray()
|
|
199
|
+
self._stored = False
|
|
200
|
+
self._released = False
|
|
201
|
+
|
|
202
|
+
def __iter__(self) -> Iterable[bytes]:
|
|
203
|
+
completed = False
|
|
204
|
+
try:
|
|
205
|
+
for chunk in self._stream:
|
|
206
|
+
self._chunks.extend(chunk)
|
|
207
|
+
if self._terminal.feed(chunk):
|
|
208
|
+
self._store_once()
|
|
209
|
+
yield chunk
|
|
210
|
+
completed = True
|
|
211
|
+
self._store_once()
|
|
212
|
+
finally:
|
|
213
|
+
if not completed:
|
|
214
|
+
if self._terminal.complete:
|
|
215
|
+
self._store_once()
|
|
216
|
+
if not self._stored:
|
|
217
|
+
self._chunks.clear()
|
|
218
|
+
self._release_once()
|
|
219
|
+
|
|
220
|
+
def _store_once(self) -> None:
|
|
221
|
+
if not self._stored:
|
|
222
|
+
self._stored = True
|
|
223
|
+
self._on_complete(bytes(self._chunks))
|
|
224
|
+
|
|
225
|
+
def _release_once(self) -> None:
|
|
226
|
+
if not self._released:
|
|
227
|
+
self._released = True
|
|
228
|
+
self._release()
|
|
229
|
+
|
|
230
|
+
def close(self) -> None:
|
|
231
|
+
try:
|
|
232
|
+
self._stream.close()
|
|
233
|
+
finally:
|
|
234
|
+
self._release_once()
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class _CachingAsyncStream(_http.AsyncByteStream):
|
|
238
|
+
def __init__(
|
|
239
|
+
self,
|
|
240
|
+
stream: Any,
|
|
241
|
+
on_complete: Callable[[bytes], Awaitable[None]],
|
|
242
|
+
release: Callable[[], None],
|
|
243
|
+
content_encoding: str,
|
|
244
|
+
):
|
|
245
|
+
self._stream = stream
|
|
246
|
+
self._on_complete = on_complete
|
|
247
|
+
self._release = release
|
|
248
|
+
self._content_encoding = content_encoding
|
|
249
|
+
self._terminal = _SSETerminalDetector(content_encoding)
|
|
250
|
+
self._chunks = bytearray()
|
|
251
|
+
self._stored = False
|
|
252
|
+
self._released = False
|
|
253
|
+
|
|
254
|
+
async def __aiter__(self) -> Any:
|
|
255
|
+
completed = False
|
|
256
|
+
try:
|
|
257
|
+
async for chunk in self._stream:
|
|
258
|
+
self._chunks.extend(chunk)
|
|
259
|
+
if self._terminal.feed(chunk):
|
|
260
|
+
await self._store_once()
|
|
261
|
+
yield chunk
|
|
262
|
+
completed = True
|
|
263
|
+
await self._store_once()
|
|
264
|
+
finally:
|
|
265
|
+
if not completed:
|
|
266
|
+
if self._terminal.complete:
|
|
267
|
+
await self._store_once()
|
|
268
|
+
if not self._stored:
|
|
269
|
+
self._chunks.clear()
|
|
270
|
+
self._release_once()
|
|
271
|
+
|
|
272
|
+
async def _store_once(self) -> None:
|
|
273
|
+
if not self._stored:
|
|
274
|
+
self._stored = True
|
|
275
|
+
await self._on_complete(bytes(self._chunks))
|
|
276
|
+
|
|
277
|
+
def _release_once(self) -> None:
|
|
278
|
+
if not self._released:
|
|
279
|
+
self._released = True
|
|
280
|
+
self._release()
|
|
281
|
+
|
|
282
|
+
async def aclose(self) -> None:
|
|
283
|
+
try:
|
|
284
|
+
if self._terminal.complete:
|
|
285
|
+
await self._store_once()
|
|
286
|
+
await self._stream.aclose()
|
|
287
|
+
finally:
|
|
288
|
+
self._release_once()
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class CachingSyncClient(_http.Client):
|
|
292
|
+
"""An ``httpx.Client``-compatible proxy around any SDK HTTP client."""
|
|
293
|
+
|
|
294
|
+
def __init__(self, inner: Any, settings: CacheSettings):
|
|
295
|
+
self._cached_openai_inner = inner
|
|
296
|
+
self._cached_openai_cache = _CacheAccess(settings)
|
|
297
|
+
self._cached_openai_settings = settings
|
|
298
|
+
|
|
299
|
+
@property
|
|
300
|
+
def timeout(self) -> Any:
|
|
301
|
+
return self._cached_openai_inner.timeout
|
|
302
|
+
|
|
303
|
+
@timeout.setter
|
|
304
|
+
def timeout(self, value: Any) -> None:
|
|
305
|
+
self._cached_openai_inner.timeout = value
|
|
306
|
+
|
|
307
|
+
@property
|
|
308
|
+
def is_closed(self) -> bool:
|
|
309
|
+
return bool(self._cached_openai_inner.is_closed)
|
|
310
|
+
|
|
311
|
+
def build_request(self, *args: Any, **kwargs: Any) -> Any:
|
|
312
|
+
return self._cached_openai_inner.build_request(*args, **kwargs)
|
|
313
|
+
|
|
314
|
+
def send(self, request: Any, *args: Any, **kwargs: Any) -> Any:
|
|
315
|
+
if not self._cached_openai_settings.enabled:
|
|
316
|
+
return self._cached_openai_inner.send(request, *args, **kwargs)
|
|
317
|
+
try:
|
|
318
|
+
body = request.read()
|
|
319
|
+
cache_key = request_fingerprint(
|
|
320
|
+
str(request.method), str(request.url), request.headers, body
|
|
321
|
+
)
|
|
322
|
+
except Exception as error:
|
|
323
|
+
_warn_once(
|
|
324
|
+
"openai-sqlite-cache could not fingerprint a request; "
|
|
325
|
+
f"bypassing cache: {error}"
|
|
326
|
+
)
|
|
327
|
+
return self._cached_openai_inner.send(request, *args, **kwargs)
|
|
328
|
+
if cache_key is None:
|
|
329
|
+
return self._cached_openai_inner.send(request, *args, **kwargs)
|
|
330
|
+
|
|
331
|
+
lock = _sync_lock(cache_key)
|
|
332
|
+
lock.acquire()
|
|
333
|
+
release_here = True
|
|
334
|
+
try:
|
|
335
|
+
cached = self._cached_openai_cache.get(cache_key)
|
|
336
|
+
if cached is not None:
|
|
337
|
+
try:
|
|
338
|
+
return self._cached_openai_cache.response(cached, request)
|
|
339
|
+
except Exception as error:
|
|
340
|
+
_warn_once(
|
|
341
|
+
"openai-sqlite-cache could not reconstruct a cached response; "
|
|
342
|
+
f"requesting upstream instead: {error}"
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
response = self._cached_openai_inner.send(request, *args, **kwargs)
|
|
346
|
+
if not 200 <= int(response.status_code) < 300:
|
|
347
|
+
return response
|
|
348
|
+
if kwargs.get("stream", False):
|
|
349
|
+
try:
|
|
350
|
+
buffered_content = bytes(response.content)
|
|
351
|
+
except Exception:
|
|
352
|
+
release_here = False
|
|
353
|
+
response.stream = _CachingSyncStream(
|
|
354
|
+
response.stream,
|
|
355
|
+
lambda content: self._cached_openai_cache.put(
|
|
356
|
+
cache_key, response, content, raw_body=True
|
|
357
|
+
),
|
|
358
|
+
lock.release,
|
|
359
|
+
str(response.headers.get("content-encoding", "")),
|
|
360
|
+
)
|
|
361
|
+
else:
|
|
362
|
+
self._cached_openai_cache.put(cache_key, response, buffered_content)
|
|
363
|
+
else:
|
|
364
|
+
self._cached_openai_cache.put(
|
|
365
|
+
cache_key, response, bytes(response.content)
|
|
366
|
+
)
|
|
367
|
+
return response
|
|
368
|
+
finally:
|
|
369
|
+
if release_here:
|
|
370
|
+
lock.release()
|
|
371
|
+
|
|
372
|
+
def close(self) -> None:
|
|
373
|
+
self._cached_openai_inner.close()
|
|
374
|
+
|
|
375
|
+
def __enter__(self) -> "CachingSyncClient":
|
|
376
|
+
self._cached_openai_inner.__enter__()
|
|
377
|
+
return self
|
|
378
|
+
|
|
379
|
+
def __exit__(self, *args: Any) -> Any:
|
|
380
|
+
return self._cached_openai_inner.__exit__(*args)
|
|
381
|
+
|
|
382
|
+
def __getattr__(self, name: str) -> Any:
|
|
383
|
+
return getattr(self._cached_openai_inner, name)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
class CachingAsyncClient(_http.AsyncClient):
|
|
387
|
+
"""An ``httpx.AsyncClient``-compatible proxy around any SDK HTTP client."""
|
|
388
|
+
|
|
389
|
+
def __init__(self, inner: Any, settings: CacheSettings):
|
|
390
|
+
self._cached_openai_inner = inner
|
|
391
|
+
self._cached_openai_cache = _CacheAccess(settings)
|
|
392
|
+
self._cached_openai_settings = settings
|
|
393
|
+
|
|
394
|
+
@property
|
|
395
|
+
def timeout(self) -> Any:
|
|
396
|
+
return self._cached_openai_inner.timeout
|
|
397
|
+
|
|
398
|
+
@timeout.setter
|
|
399
|
+
def timeout(self, value: Any) -> None:
|
|
400
|
+
self._cached_openai_inner.timeout = value
|
|
401
|
+
|
|
402
|
+
@property
|
|
403
|
+
def is_closed(self) -> bool:
|
|
404
|
+
return bool(self._cached_openai_inner.is_closed)
|
|
405
|
+
|
|
406
|
+
def build_request(self, *args: Any, **kwargs: Any) -> Any:
|
|
407
|
+
return self._cached_openai_inner.build_request(*args, **kwargs)
|
|
408
|
+
|
|
409
|
+
async def send(self, request: Any, *args: Any, **kwargs: Any) -> Any:
|
|
410
|
+
if not self._cached_openai_settings.enabled:
|
|
411
|
+
return await self._cached_openai_inner.send(request, *args, **kwargs)
|
|
412
|
+
try:
|
|
413
|
+
body = await request.aread()
|
|
414
|
+
cache_key = request_fingerprint(
|
|
415
|
+
str(request.method), str(request.url), request.headers, body
|
|
416
|
+
)
|
|
417
|
+
except Exception as error:
|
|
418
|
+
_warn_once(
|
|
419
|
+
"openai-sqlite-cache could not fingerprint a request; "
|
|
420
|
+
f"bypassing cache: {error}"
|
|
421
|
+
)
|
|
422
|
+
return await self._cached_openai_inner.send(request, *args, **kwargs)
|
|
423
|
+
if cache_key is None:
|
|
424
|
+
return await self._cached_openai_inner.send(request, *args, **kwargs)
|
|
425
|
+
|
|
426
|
+
lock = _async_lock(cache_key)
|
|
427
|
+
await lock.acquire()
|
|
428
|
+
release_here = True
|
|
429
|
+
try:
|
|
430
|
+
cached = await asyncio.to_thread(self._cached_openai_cache.get, cache_key)
|
|
431
|
+
if cached is not None:
|
|
432
|
+
try:
|
|
433
|
+
return self._cached_openai_cache.response(cached, request)
|
|
434
|
+
except Exception as error:
|
|
435
|
+
_warn_once(
|
|
436
|
+
"openai-sqlite-cache could not reconstruct a cached response; "
|
|
437
|
+
f"requesting upstream instead: {error}"
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
response = await self._cached_openai_inner.send(request, *args, **kwargs)
|
|
441
|
+
if not 200 <= int(response.status_code) < 300:
|
|
442
|
+
return response
|
|
443
|
+
if kwargs.get("stream", False):
|
|
444
|
+
try:
|
|
445
|
+
buffered_content = bytes(response.content)
|
|
446
|
+
except Exception:
|
|
447
|
+
release_here = False
|
|
448
|
+
|
|
449
|
+
async def store(content: bytes) -> None:
|
|
450
|
+
try:
|
|
451
|
+
await asyncio.to_thread(
|
|
452
|
+
self._cached_openai_cache.put,
|
|
453
|
+
cache_key,
|
|
454
|
+
response,
|
|
455
|
+
content,
|
|
456
|
+
raw_body=True,
|
|
457
|
+
)
|
|
458
|
+
except Exception as error:
|
|
459
|
+
_warn_once(
|
|
460
|
+
f"openai-sqlite-cache cache write failed: {error}"
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
response.stream = _CachingAsyncStream(
|
|
464
|
+
response.stream,
|
|
465
|
+
store,
|
|
466
|
+
lock.release,
|
|
467
|
+
str(response.headers.get("content-encoding", "")),
|
|
468
|
+
)
|
|
469
|
+
else:
|
|
470
|
+
await asyncio.to_thread(
|
|
471
|
+
self._cached_openai_cache.put,
|
|
472
|
+
cache_key,
|
|
473
|
+
response,
|
|
474
|
+
buffered_content,
|
|
475
|
+
)
|
|
476
|
+
else:
|
|
477
|
+
await asyncio.to_thread(
|
|
478
|
+
self._cached_openai_cache.put,
|
|
479
|
+
cache_key,
|
|
480
|
+
response,
|
|
481
|
+
bytes(response.content),
|
|
482
|
+
)
|
|
483
|
+
return response
|
|
484
|
+
finally:
|
|
485
|
+
if release_here:
|
|
486
|
+
lock.release()
|
|
487
|
+
|
|
488
|
+
async def aclose(self) -> None:
|
|
489
|
+
await self._cached_openai_inner.aclose()
|
|
490
|
+
|
|
491
|
+
async def __aenter__(self) -> "CachingAsyncClient":
|
|
492
|
+
await self._cached_openai_inner.__aenter__()
|
|
493
|
+
return self
|
|
494
|
+
|
|
495
|
+
async def __aexit__(self, *args: Any) -> Any:
|
|
496
|
+
return await self._cached_openai_inner.__aexit__(*args)
|
|
497
|
+
|
|
498
|
+
def __getattr__(self, name: str) -> Any:
|
|
499
|
+
return getattr(self._cached_openai_inner, name)
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def wrap_sync_client(client: Any, settings: CacheSettings) -> Any:
|
|
503
|
+
if isinstance(client, CachingSyncClient):
|
|
504
|
+
if client._cached_openai_settings == settings:
|
|
505
|
+
return client
|
|
506
|
+
client = client._cached_openai_inner
|
|
507
|
+
return CachingSyncClient(client, settings)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def wrap_async_client(client: Any, settings: CacheSettings) -> Any:
|
|
511
|
+
if isinstance(client, CachingAsyncClient):
|
|
512
|
+
if client._cached_openai_settings == settings:
|
|
513
|
+
return client
|
|
514
|
+
client = client._cached_openai_inner
|
|
515
|
+
return CachingAsyncClient(client, settings)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Lazy aliases for public submodules from the upstream ``openai`` package."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import importlib.abc
|
|
7
|
+
import importlib.util
|
|
8
|
+
import sys
|
|
9
|
+
from types import ModuleType
|
|
10
|
+
from typing import Any, Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class _AliasModule(ModuleType):
|
|
14
|
+
def __init__(self, alias: str, target_name: str):
|
|
15
|
+
super().__init__(alias)
|
|
16
|
+
target = importlib.import_module(target_name)
|
|
17
|
+
self.__dict__["_alias_target"] = target
|
|
18
|
+
self.__dict__["__doc__"] = getattr(target, "__doc__", None)
|
|
19
|
+
exported = getattr(target, "__all__", None)
|
|
20
|
+
self.__dict__["__all__"] = (
|
|
21
|
+
list(exported)
|
|
22
|
+
if exported is not None
|
|
23
|
+
else [name for name in dir(target) if not name.startswith("_")]
|
|
24
|
+
)
|
|
25
|
+
self.__dict__["__file__"] = getattr(target, "__file__", None)
|
|
26
|
+
if hasattr(target, "__path__"):
|
|
27
|
+
self.__dict__["__path__"] = list(target.__path__)
|
|
28
|
+
|
|
29
|
+
def __getattr__(self, name: str) -> Any:
|
|
30
|
+
return getattr(self.__dict__["_alias_target"], name)
|
|
31
|
+
|
|
32
|
+
def __dir__(self) -> list:
|
|
33
|
+
return sorted(set(self.__dict__) | set(dir(self.__dict__["_alias_target"])))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class _AliasLoader(importlib.abc.Loader):
|
|
37
|
+
def __init__(self, alias: str, target: str):
|
|
38
|
+
self.alias = alias
|
|
39
|
+
self.target = target
|
|
40
|
+
|
|
41
|
+
def create_module(self, spec: Any) -> ModuleType:
|
|
42
|
+
return _AliasModule(self.alias, self.target)
|
|
43
|
+
|
|
44
|
+
def exec_module(self, module: ModuleType) -> None:
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class _AliasFinder(importlib.abc.MetaPathFinder):
|
|
49
|
+
def __init__(self, alias_root: str, target_root: str):
|
|
50
|
+
self.alias_root = alias_root
|
|
51
|
+
self.target_root = target_root
|
|
52
|
+
|
|
53
|
+
def find_spec(
|
|
54
|
+
self, fullname: str, path: Any = None, target: Any = None
|
|
55
|
+
) -> Optional[Any]:
|
|
56
|
+
prefix = self.alias_root + "."
|
|
57
|
+
if not fullname.startswith(prefix):
|
|
58
|
+
return None
|
|
59
|
+
target_name = self.target_root + fullname[len(self.alias_root) :]
|
|
60
|
+
target_spec = importlib.util.find_spec(target_name)
|
|
61
|
+
if target_spec is None:
|
|
62
|
+
return None
|
|
63
|
+
is_package = target_spec.submodule_search_locations is not None
|
|
64
|
+
return importlib.util.spec_from_loader(
|
|
65
|
+
fullname,
|
|
66
|
+
_AliasLoader(fullname, target_name),
|
|
67
|
+
is_package=is_package,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def install_module_alias(alias_root: str, target_root: str) -> ModuleType:
|
|
72
|
+
"""Install a lazy module tree alias while preserving exported class identity."""
|
|
73
|
+
|
|
74
|
+
existing = sys.modules.get(alias_root)
|
|
75
|
+
if isinstance(existing, _AliasModule):
|
|
76
|
+
return existing
|
|
77
|
+
proxy = _AliasModule(alias_root, target_root)
|
|
78
|
+
sys.modules[alias_root] = proxy
|
|
79
|
+
if not any(
|
|
80
|
+
isinstance(finder, _AliasFinder)
|
|
81
|
+
and finder.alias_root == alias_root
|
|
82
|
+
and finder.target_root == target_root
|
|
83
|
+
for finder in sys.meta_path
|
|
84
|
+
):
|
|
85
|
+
sys.meta_path.insert(0, _AliasFinder(alias_root, target_root))
|
|
86
|
+
return proxy
|