From 25ebd8ca81c078489b5a1e11f7026f84d272673a Mon Sep 17 00:00:00 2001 From: abhinav7x94 Date: Sun, 16 Aug 2026 09:07:55 +0530 Subject: [PATCH] fix(openai-sdk-python): restore complete async middleware update --- .../src/supermemory_openai/middleware.py | 549 +++++++++++++----- .../tests/test_client_isolation.py | 150 +++++ 2 files changed, 545 insertions(+), 154 deletions(-) diff --git a/packages/openai-sdk-python/src/supermemory_openai/middleware.py b/packages/openai-sdk-python/src/supermemory_openai/middleware.py index c377c38d..01315369 100644 --- a/packages/openai-sdk-python/src/supermemory_openai/middleware.py +++ b/packages/openai-sdk-python/src/supermemory_openai/middleware.py @@ -439,164 +439,405 @@ class SupermemoryOpenAIWrapper: def _create_client_variant_facade(self, name: str) -> _ResourceFacade: """Wrap completions reached through a client response variant.""" client_resource = getattr(self._client, name) - chat_resource = client_resource.chaëN½¶‰žËkºwµçXÛÛ\][ÛœË\˜]È‹˜]רܙX]WÙ˜XÝÜžBˆ -BˆÛY[� ˜Ú] �Ú]ܘ]×Ü™\ÜÛœÙHHÚ[\S˜[Y\ÜXÙJˆÛÛ\][ÛœÏXÛÛ\][Û—Ü™\ÛÝ\˜ÙJ˜Ú] \˜]È‹˜]רܙX]WÙ˜XÝÜžJBˆ -BˆÛY[� �Ú]ܘ]×Ü™\ÜÛœÙHHÚ[\S˜[Y\ÜXÙJˆÚ]TÚ[\S˜[Y\ÜXÙJˆÛÛ\][ÛœÏXÛÛ\][Û—Ü™\ÛÝ\˜ÙJ˜ÛY[� \˜]È‹˜]רܙX]WÙ˜XÝÜžJBˆ -Bˆ -BˆÛY[� ˜Ú] ˜ÛÛ\][ÛœË�Ú]ÜÝ™X[Z[™×Ü™\ÜÛœÙHHÛÛ\][Û—Ü™\ÛÝ\˜ÙJˆ˜ÛÛ\][ÛœË\Ý™X[H‹Ý™X[Z[™×ØÜ™X]WÙ˜XÝÜžBˆ -BˆÛY[� ˜Ú] �Ú]ÜÝ™X[Z[™×Ü™\ÜÛœÙHHÚ[\S˜[Y\ÜXÙJˆÛÛ\][ÛœÏXÛÛ\][Û—Ü™\ÛÝ\˜ÙJ˜Ú] \Ý™X[H‹Ý™X[Z[™×ØÜ™X]WÙ˜XÝÜžJBˆ -BˆÛY[� �Ú]ÜÝ™X[Z[™×Ü™\ÜÛœÙHHÚ[\S˜[Y\ÜXÙJˆÚ]TÚ[\S˜[Y\ÜXÙJˆÛÛ\][ÛœÏXÛÛ\][Û—Ü™\ÛÝ\˜ÙJ˜ÛY[� \Ý™X[H‹Ý™X[Z[™×ØÜ™X]WÙ˜XÝÜžJBˆ -Bˆ -Bˆ™]\›ˆØ[‚‚™Yˆ™\ÜÛœÙWݘ\šX[�ØÜ™X]\ÊÛY[�ˆ[žK˜[YNˆÝŠH Oˆ\ÝÐ[žWN‚ˆ™]\›ˆˆÙ]]ŠÛY[� ˜Ú] ˜ÛÛ\][ۜ˘[YJK˜Ü™X]KˆÙ]]ŠÛY[� ˜Ú] ˜[YJK˜ÛÛ\][ۜ˘ܙX]KˆÙ]]ŠÛY[� ˜[YJK˜Ú] ˜ÛÛ\][ۜ˘ܙX]KˆB‚‚”‘PSÐTÖS�×ÐÓÓTUSÓ—ÔUÈH -ˆ››Ü›X[‹ˆ˜ÛÛ\][ۜ˜˜]È‹ˆ˜Ú] œ˜]È‹ˆ˜ÛY[� œ˜]È‹ˆ˜ÛÛ\][ۜ˜Ý™X[Z[™È‹ˆ˜Ú] œÝ™X[Z[™È‹ˆ˜ÛY[� œÝ™X[Z[™È‹ŠB‚‚™Yˆ™X[Ø\Þ[˜×ØÛÛ\][Û—ØÜ™X]JÛY[�ˆ[žK]ˆÝŠH Oˆ[žN‚ˆYˆ]OH››Ü›X[Ž‚ˆ™]\›ˆÛY[� ˜Ú] ˜ÛÛ\][ۜ˘ܙX]BˆYˆ]OH˜ÛÛ\][ۜ˜˜]ÈŽ‚ˆ™]\›ˆÛY[� ˜Ú] ˜ÛÛ\][ÛœË�Ú]ܘ]×Ü™\ÜÛœÙK˜Ü™X]BˆYˆ]OH˜Ú] œ˜]ÈŽ‚ˆ™]\›ˆÛY[� ˜Ú] �Ú]ܘ]×Ü™\ÜÛœÙK˜ÛÛ\][ۜ˘ܙX]BˆYˆ]OH˜ÛY[� œ˜]ÈŽ‚ˆ™]\›ˆÛY[� �Ú]ܘ]×Ü™\ÜÛœÙK˜Ú] ˜ÛÛ\][ۜ˘ܙX]BˆYˆ]OH˜ÛÛ\][ۜ˜Ý™X[Z[™ÈŽ‚ˆ™]\›ˆÛY[� ˜Ú] ˜ÛÛ\][ÛœË�Ú]ÜÝ™X[Z[™×Ü™\ÜÛœÙK˜Ü™X]BˆYˆ]OH˜Ú] œÝ™X[Z[™ÈŽ‚ˆ™]\›ˆÛY[� ˜Ú] �Ú]ÜÝ™X[Z[™×Ü™\ÜÛœÙK˜ÛÛ\][ۜ˘ܙX]BˆYˆ]OH˜ÛY[� œÝ™X[Z[™ÈŽ‚ˆ™]\›ˆÛY[� �Ú]ÜÝ™X[Z[™×Ü™\ÜÛœÙK˜Ú] ˜ÛÛ\][ۜ˘ܙX]Bˆ˜Z\ÙH\ÜÙ\�[Û‘\œ›ÜŠˆ•[šÛ›ÝÛˆÛÛ\][Ûˆ]ˆÜ]HŠB‚‚�]\Ý ™š^\™J]]Ý\ÙOU�YJHÈ\NˆYۛܙVÝ[�\Y YXÛܘ]Ü—B™YˆÝ\\›Y[[ÜžWØ\WÚÙ^J -H OˆÙ[™\˜]Ü–Ó›Û™K›Û™K›Û™WN‚ˆÚ]]Ú ™XÝ -ÜË™[�š\›Û‹È”ÕTT“QSSÔ–WÐTWÒÑVHŽˆ�\Ý ZÙ^HŸJN‚ˆZY[‚‚™Yˆ\ÝÜÚ\™YÜÞ[˜×ØÛY[�ÙÙ\×Û›ÝÜÝXÚ×Ý[˜[�ÛZY]Ø\™J -H Oˆ›Û™N‚ˆ˜\ÙWØÛY[� ÜšYÚ[˜[ØÜ™X]HHÜ™X]WÜÞ[˜×ØÛY[� + chat_resource = client_resource.chat + completions_resource = chat_resource.completions + return _ResourceFacade( + client_resource, + chat=_ResourceFacade( + chat_resource, + completions=_ResourceFacade( + completions_resource, + create=self._create_completion_method( + completions_resource.create, + streaming_response=name == "with_streaming_response", + ), + ), + ), + ) + + def _create_completion_method( + self, + original_create: Any, + *, + streaming_response: bool = False, + ) -> Any: + """Wrap one completion create implementation with memory injection.""" + if self._is_async_client and streaming_response: -BˆÛÚÝ\Έ\ÝÜÝ—HH×B‚ˆ\Þ[˜ÈYˆ˜ZÙWÜ›Û\ -ˆY\ÜØYÙ\Έ\ÝÐ[žWKˆÛÛ�Z[™\—ÝYΈÝ‹ˆÙÙÙ\Žˆ[žKˆ[ÙNˆ[žKˆ\WÚÙ^NˆÝ‹ˆ -H Oˆ\ÝÐ[žWN‚ˆÛÚÝ\˘\[™ -ÛÛ�Z[™\—ÝYÊBˆ™]\›ˆˆÈœ›ÛHŽˆœÞ\Ý[H‹˜ÛÛ�[�ŽˆˆœÙXÜ™] ^ØÛÛ�Z[™\—ÝYßHŸKˆ -›Y\ÜØYÙ\ˈB‚ˆÚ]]Ú -ˆœÝ\\›Y[[ÜžWÛÜ[˜ZK›ZY]Ø\™KœÝ\\›Y[[ÜžK”Ý\\›Y[[ÜžH‹ˆ™]\›—ݘ[YOS[ØÚÊ -Kˆ -K]Ú -ˆœÝ\\›Y[[ÜžWÛÜ[˜ZK›ZY]Ø\™K˜YÜÞ\Ý[WÜ›Û\‹ˆÚYWÙY™™XÝY˜ZÙWÜ›Û\ ˆ -N‚ˆ[˜[�ØNˆ[žHHÚ]ÜÝ\\›Y[[ÜžJˆ˜\ÙWØÛY[� ˆZY]Ø\™WÛÜ[ÛœÊ�[˜[� XHŠKˆ -Bˆ[˜[�ØŽˆ[žHHÚ]ÜÝ\\›Y[[ÜžJˆ˜\ÙWØÛY[� ˆZY]Ø\™WÛÜ[ÛœÊ�[˜[� XˆŠKˆ -B‚ˆ\ÜÙ\�˜\ÙWØÛY[� ˜Ú] ˜ÛÛ\][ۜ˘ܙX]H\ÈÜšYÚ[˜[ØÜ™X]Bˆ\ÜÙ\�[˜[�ØK˜Ú]\țݘ\ÙWØÛY[� ˜Ú]ˆ\ÜÙ\�[˜[�Ø‹˜Ú] ›X\šÙ\ˆOH˜Ú] [X\šÙ\ˆ‚ˆ\ÜÙ\�[˜[�Ø‹˜Ú] ˜ÛÛ\][ۜ˛X\šÙ\ˆOH˜ÛÛ\][ÛœË[X\šÙ\ˆ‚‚ˆ[˜[�Ø‹˜Ú] ˜ÛÛ\][ۜ˘ܙX]Jˆ[Ù[H™Ü ]\Ý‹ˆY\ÜØYÙ\ÏVÞÈœ›ÛHŽˆ�\Ù\ˆ‹˜ÛÛ�[�Žˆœš]˜]HŸWKˆ -B‚ˆ\ÜÙ\�ÛÚÝ\ÈOHÈ�[˜[� Xˆ—BˆY\ÜØYÙ\ÈHÜšYÚ[˜[ØÜ™X]K˜Ø[Ø\™ÜËšÝØ\™ÜÖÈ›Y\ÜØYÙ\È—Bˆ\ÜÙ\�Y\ÜØYÙ\ÖÌVȘÛÛ�[�—HOHœÙXÜ™] ][˜[� Xˆ‚ˆ\ÜÙ\�[ -�[˜[� XHˆ›Ý[ˆÝŠY\ÜØYÙJH›ÜˆY\ÜØYÙH[ˆY\ÜØYÙ\ÊB‚ˆ˜\ÙWØÛY[� ˜Ú] ˜ÛÛ\][ۜ˘ܙX]Jˆ[Ù[H™Ü ]\Ý‹ˆY\ÜØYÙ\ÏVÞÈœ›ÛHŽˆ�\Ù\ˆ‹˜ÛÛ�[�Žˆ�[�ܘ\YŸWKˆ -Bˆ\ÜÙ\�ÛÚÝ\ÈOHÈ�[˜[� Xˆ—B‚‚™Yˆ\ÝÜ™]ܘ\[™×ØWÙ˜XØYWÜ™XÛÝ™\œ×ÝWÜš\Ý[™WØÛY[� + def create_streaming_with_memory( + **kwargs: Any, + ) -> _AsyncMemoryResponseContextManager: + return _AsyncMemoryResponseContextManager( + self, + original_create, + kwargs, + ) -H Oˆ›Û™N‚ˆ˜\ÙWØÛY[� ÜšYÚ[˜[ØÜ™X]HHÜ™X]WÜÞ[˜×ØÛY[� + return create_streaming_with_memory -BˆÛÚÝ\Έ\ÝÜÝ—HH×B‚ˆ\Þ[˜ÈYˆ˜ZÙWÜ›Û\ -ˆY\ÜØYÙ\Έ\ÝÐ[žWKˆÛÛ�Z[™\—ÝYΈÝ‹ˆÙÙÙ\Žˆ[žKˆ[ÙNˆ[žKˆ\WÚÙ^NˆÝ‹ˆ -H Oˆ\ÝÐ[žWN‚ˆÛÚÝ\˘\[™ -ÛÛ�Z[™\—ÝYÊBˆ™]\›ˆˆÈœ›ÛHŽˆœÞ\Ý[H‹˜ÛÛ�[�ŽˆˆœÙXÜ™] ^ØÛÛ�Z[™\—ÝYßHŸKˆ -›Y\ÜØYÙ\ˈB‚ˆÚ]]Ú -ˆœÝ\\›Y[[ÜžWÛÜ[˜ZK›ZY]Ø\™KœÝ\\›Y[[ÜžK”Ý\\›Y[[ÜžH‹ˆ™]\›—ݘ[YOS[ØÚÊ -Kˆ -K]Ú -ˆœÝ\\›Y[[ÜžWÛÜ[˜ZK›ZY]Ø\™K˜YÜÞ\Ý[WÜ›Û\‹ˆÚYWÙY™™XÝY˜ZÙWÜ›Û\ ˆ -N‚ˆ[˜[�ØNˆ[žHHÚ]ÜÝ\\›Y[[ÜžJˆ˜\ÙWØÛY[� ˆZY]Ø\™WÛÜ[ÛœÊ�[˜[� XHŠKˆ -Bˆ[˜[�ØŽˆ[žHHÚ]ÜÝ\\›Y[[ÜžJˆ[˜[�ØKˆZY]Ø\™WÛÜ[ÛœÊ�[˜[� XˆŠKˆ -B‚ˆ[˜[�Ø‹˜Ú] ˜ÛÛ\][ۜ˘ܙX]Jˆ[Ù[H™Ü ]\Ý‹ˆY\ÜØYÙ\ÏVÞÈœ›ÛHŽˆ�\Ù\ˆ‹˜ÛÛ�[�Žˆœš]˜]HŸWKˆ -B‚ˆ\ÜÙ\�ÛÚÝ\ÈOHÈ�[˜[� Xˆ—Bˆ\ÜÙ\�ÜšYÚ[˜[ØÜ™X]K˜Ø[ØÛÝ[�OH Bˆ\ÜÙ\�˜\ÙWØÛY[� ˜Ú] ˜ÛÛ\][ۜ˘ܙX]H\ÈÜšYÚ[˜[ØÜ™X]B‚‚™Yˆ\Ýܘ]ר[™ÜÝ™X[Z[™×Ü™\ÜÛœÙWÙ˜XØY\×Ü™[XZ[—ÛY[[ÜžWØ]Ø\™J -H Oˆ›Û™N‚ˆ˜\ÙWØÛY[� ÈHÜ™X]WÜÞ[˜×ØÛY[� + if self._is_async_client: -BˆØ[ÈH]XÚÜ™\ÜÛœÙWݘ\šX[�ʈ˜\ÙWØÛY[� ˆ[X™HX™[ˆ[ØÚÊ™]\›—ݘ[YOT˜]Ô™\ÜÛœÙJX™[ -JKˆ[X™HX™[ˆ[ØÚÊ™]\›—ݘ[YOTÞ[˜ÔÝ™X[PÛÛ�^ -X™[ -JKˆ -BˆÛÚÝ\Έ\ÝÜÝ—HH×B‚ˆ\Þ[˜ÈYˆ˜ZÙWÜ›Û\ -ˆY\ÜØYÙ\Έ\ÝÐ[žWKˆÛÛ�Z[™\—ÝYΈÝ‹ˆÙÙÙ\Žˆ[žKˆ[ÙNˆ[žKˆ\WÚÙ^NˆÝ‹ˆ -H Oˆ\ÝÐ[žWN‚ˆÛÚÝ\˘\[™ -ÛÛ�Z[™\—ÝYÊBˆ™]\›ˆˆÈœ›ÛHŽˆœÞ\Ý[H‹˜ÛÛ�[�ŽˆˆœÙXÜ™] ^ØÛÛ�Z[™\—ÝYßHŸKˆ -›Y\ÜØYÙ\ˈB‚ˆÚ]]Ú -ˆœÝ\\›Y[[ÜžWÛÜ[˜ZK›ZY]Ø\™KœÝ\\›Y[[ÜžK”Ý\\›Y[[ÜžH‹ˆ™]\›—ݘ[YOS[ØÚÊ -Kˆ -K]Ú -ˆœÝ\\›Y[[ÜžWÛÜ[˜ZK›ZY]Ø\™K˜YÜÞ\Ý[WÜ›Û\‹ˆÚYWÙY™™XÝY˜ZÙWÜ›Û\ ˆ -N‚ˆ[˜[�ØŽˆ[žHHÚ]ÜÝ\\›Y[[ÜžJˆ˜\ÙWØÛY[� ˆZY]Ø\™WÛÜ[ÛœÊ�[˜[� XˆŠKˆ -B‚ˆ›ÜˆÜ™X]H[ˆ™\ÜÛœÙWݘ\šX[�ØÜ™X]\Ê[˜[�Ø‹�Ú]ܘ]×Ü™\ÜÛœÙHŠN‚ˆ™\ÜÛœÙHHÜ™X]Jˆ[Ù[H™Ü ]\Ý‹ˆY\ÜØYÙ\ÏVÞÈœ›ÛHŽˆ�\Ù\ˆ‹˜ÛÛ�[�Žˆœ˜]ÈŸWKˆ -Bˆ\ÜÙ\�™\ÜÛœÙKœ\œÙJ -KœÝ\�ÝÚ] -œ\œÙY HŠB‚ˆ›ÜˆÜ™X]H[ˆ™\ÜÛœÙWݘ\šX[�ØÜ™X]\Ê[˜[�Ø‹�Ú]ÜÝ™X[Z[™×Ü™\ÜÛœÙHŠN‚ˆÚ]Ü™X]Jˆ[Ù[H™Ü ]\Ý‹ˆY\ÜØYÙ\ÏVÞÈœ›ÛHŽˆ�\Ù\ˆ‹˜ÛÛ�[�ŽˆœÝ™X[Z[™ÈŸWKˆ -H\ÈÝ™X[N‚ˆ\ÜÙ\�Ý™X[K›X™[ ™[™ÝÚ] -œÝ™X[HŠB‚ˆ\ÜÙ\�ÛÚÝ\ÈOHÈ�[˜[� Xˆ—H -ˆ ‚ˆ›ÜˆÜ™X]H[ˆØ[Ë�˜[Y\Ê -N‚ˆ\ÜÙ\�œÙXÜ™] ][˜[� Xˆˆ[ˆÝŠÜ™X]K˜Ø[Ø\™ÜËšÝØ\™ÜÖÈ›Y\ÜØYÙ\È—JB‚‚™Yˆ\ÝØ\Þ[˜×ܘ]ר[™ÜÝ™X[Z[™×Ü™\ÜÛœÙWÜ™Yš^\×Ü™[XZ[—ÛY[[ÜžWØ]Ø\™J -H Oˆ›Û™N‚ˆ˜\ÙWØÛY[� ÈHÜ™X]WØ\Þ[˜×ØÛY[� + async def create_async_with_memory( + **kwargs: Any, + ) -> Any: + return await self._create_with_memory_async(original_create, **kwargs) -BˆØ[ÈH]XÚÜ™\ÜÛœÙWݘ\šX[�ʈ˜\ÙWØÛY[� ˆ[X™HX™[ˆ\Þ[˜Ó[ØÚÊ™]\›—ݘ[YOT˜]Ô™\ÜÛœÙJX™[ -JKˆ[X™HX™[ˆ[ØÚÊ™]\›—ݘ[YOP\Þ[˜ÔÝ™X[PÛÛ�^ -X™[ -JKˆ -BˆÛÚÝ\Έ\ÝÜÝ—HH×B‚ˆ\Þ[˜ÈYˆ˜ZÙWÜ›Û\ -ˆY\ÜØYÙ\Έ\ÝÐ[žWKˆÛÛ�Z[™\—ÝYΈÝ‹ˆÙÙÙ\Žˆ[žKˆ[ÙNˆ[žKˆ\WÚÙ^NˆÝ‹ˆ -H Oˆ\ÝÐ[žWN‚ˆÛÚÝ\˘\[™ -ÛÛ�Z[™\—ÝYÊBˆ™]\›ˆˆÈœ›ÛHŽˆœÞ\Ý[H‹˜ÛÛ�[�ŽˆˆœÙXÜ™] ^ØÛÛ�Z[™\—ÝYßHŸKˆ -›Y\ÜØYÙ\ˈB‚ˆ\Þ[˜ÈYˆØ[ܘ]ÊÜ™X]Nˆ[žJH Oˆ›Û™N‚ˆ™\ÜÛœÙHH]ØZ]Ü™X]Jˆ[Ù[H™Ü ]\Ý‹ˆY\ÜØYÙ\ÏVÞÈœ›ÛHŽˆ�\Ù\ˆ‹˜ÛÛ�[�Žˆœ˜]ÈŸWKˆ -Bˆ\ÜÙ\�™\ÜÛœÙKœ\œÙJ -KœÝ\�ÝÚ] -œ\œÙY HŠB‚ˆ\Þ[˜ÈYˆÛÛœÝ[YWÜÝ™X[JÝ™X[WØÛÛ�^ˆ[žJH Oˆ›Û™N‚ˆ\Þ[˜ÈÚ]Ý™X[WØÛÛ�^\ÈÝ™X[N‚ˆ\ÜÙ\�Ý™X[K›X™[ ™[™ÝÚ] -œÝ™X[HŠB‚ˆÚ]]Ú -ˆœÝ\\›Y[[ÜžWÛÜ[˜ZK›ZY]Ø\™KœÝ\\›Y[[ÜžK”Ý\\›Y[[ÜžH‹ˆ™]\›—ݘ[YOS[ØÚÊ -Kˆ -K]Ú -ˆœÝ\\›Y[[ÜžWÛÜ[˜ZK›ZY]Ø\™K˜YÜÞ\Ý[WÜ›Û\‹ˆÚYWÙY™™XÝY˜ZÙWÜ›Û\ ˆ -N‚ˆ[˜[�ØŽˆ[žHHÚ]ÜÝ\\›Y[[ÜžJˆ˜\ÙWØÛY[� ˆZY]Ø\™WÛÜ[ÛœÊ�[˜[� XˆŠKˆ -B‚ˆ›ÜˆÜ™X]H[ˆ™\ÜÛœÙWݘ\šX[�ØÜ™X]\Ê[˜[�Ø‹�Ú]ܘ]×Ü™\ÜÛœÙHŠN‚ˆ\Þ[˜Ú[Ëœ�[ŠØ[ܘ]ÊÜ™X]JJB‚ˆ›ÜˆÜ™X]H[ˆ™\ÜÛœÙWݘ\šX[�ØÜ™X]\Ê[˜[�Ø‹�Ú]ÜÝ™X[Z[™×Ü™\ÜÛœÙHŠN‚ˆÝ™X[WØÛÛ�^HÜ™X]Jˆ[Ù[H™Ü ]\Ý‹ˆY\ÜØYÙ\ÏVÞÈœ›ÛHŽˆ�\Ù\ˆ‹˜ÛÛ�[�ŽˆœÝ™X[Z[™ÈŸWKˆ -Bˆ\Þ[˜Ú[Ëœ�[ŠÛÛœÝ[YWÜÝ™X[JÝ™X[WØÛÛ�^ -JB‚ˆ\ÜÙ\�ÛÚÝ\ÈOHÈ�[˜[� Xˆ—H -ˆ ‚ˆ›ÜˆÜ™X]H[ˆØ[Ë�˜[Y\Ê -N‚ˆ\ÜÙ\�œÙXÜ™] ][˜[� Xˆˆ[ˆÝŠÜ™X]K˜Ø[Ø\™ÜËšÝØ\™ÜÖÈ›Y\ÜØYÙ\È—JB‚‚�]\Ý ›X\šËœ\˜[Y]š^™Jœ]‹‘PSÐTÖS�×ÐÓÓTUSÓ—ÔUÊB�]\Ý ›X\šË˜\Þ[˜Ú[˜\Þ[˜ÈYˆ\ÝÜ™X[Ø\Þ[˜×ÛÜ[˜ZWÜ]×Ý\ÙWØ\Þ[˜×ÛZY]Ø\™J]ˆÝŠH Oˆ›Û™N‚ˆÛÚÝ\Έ\ÝÜÝ—HH×BˆÜš]\Έ\ÝÝ\VÜÝ‹Ü[Û˜[ÜÝ—KÝ—WHH×BˆÙ[�ÛY\ÜØYÙ\Έ\ÝÛ\ÝÐ[žWWHH×B‚ˆ\Þ[˜ÈYˆ˜ZÙWÜ›Û\ -ˆY\ÜØYÙ\Έ\ÝÐ[žWKˆÛÛ�Z[™\—ÝYΈÝ‹ˆÙÙÙ\Žˆ[žKˆ[ÙNˆ[žKˆ\WÚÙ^NˆÝ‹ˆ -H Oˆ\ÝÐ[žWN‚ˆÛÚÝ\˘\[™ -ÛÛ�Z[™\—ÝYÊBˆ™]\›ˆˆÈœ›ÛHŽˆœÞ\Ý[H‹˜ÛÛ�[�ŽˆˆœÙXÜ™] ^ØÛÛ�Z[™\—ÝYßHŸKˆ -›Y\ÜØYÙ\ˈB‚ˆ\Þ[˜ÈYˆ˜ZÙWØYÛY[[ÜžJˆÛY[�ˆ[žKˆÛÛ�Z[™\—ÝYΈÝ‹ˆÛÛ�[�ˆÝ‹ˆÝ\ÝÛWÚYˆÜ[Û˜[ÜÝ—KˆÙÙÙ\Žˆ[žKˆ -H Oˆ›Û™N‚ˆÜš]\˘\[™ + return create_async_with_memory -ÛÛ�Z[™\—ÝYËÝ\ÝÛWÚY ÛÛ�[� -JB‚ˆYˆ[™WÜ™\]Y\Ý -™\]Y\݈ ”™\]Y\Ý -H Oˆ ”™\ÜÛœÙN‚ˆ›ÙHHœÛÛ‹›ØYÊ™\]Y\Ý ˜ÛÛ�[� -BˆÙ[�ÛY\ÜØYÙ\˘\[™ -›ÙVÈ›Y\ÜØYÙ\È—JBˆ™]\›ˆ ”™\ÜÛœÙJˆ Œ ˆ™\]Y\Ý\™\]Y\Ý ˆXY\œÏ^ȘÛÛ�[� ]\HŽˆ˜\XØ][Û‹ÚœÛÛˆŸKˆœÛÛ�^ˆšYŽˆ˜Ú]Û\ ]\Ý‹ˆ›Øš™XÝŽˆ˜Ú] ˜ÛÛ\][Ûˆ‹ˆ˜Ü™X]YŽˆ ˆ›[Ù[Žˆ™Ü ]\Ý‹ˆ˜ÚÚXÙ\ÈŽˆˆˆš[™^Žˆ ˆ›Y\ÜØYÙHŽˆÈœ›ÛHŽˆ˜\ÜÚ\Ý[�‹˜ÛÛ�[�Žˆ›ÚÈŸKˆ™š[š\ÚÜ™X\ÛÛˆŽˆœÝÜ‹ˆBˆKˆKˆ -B‚ˆØÛY[�H �\Þ[˜ÐÛY[� -˜[œÜÜ�Z “[ØÚÕ˜[œÜÜ� -[™WÜ™\]Y\Ý -JBˆ˜\ÙWØÛY[�H\Þ[˜ÓÜ[�RJ\WÚÙ^OH›Ü[˜ZK]\Ý‹ØÛY[�ZØÛY[� -B‚ˆžN‚ˆÚ]]Ú -ˆœÝ\\›Y[[ÜžWÛÜ[˜ZK›ZY]Ø\™KœÝ\\›Y[[ÜžK”Ý\\›Y[[ÜžH‹ˆ™]\›—ݘ[YOS[ØÚÊ -Kˆ -K]Ú -ˆœÝ\\›Y[[ÜžWÛÜ[˜ZK›ZY]Ø\™K˜YÜÞ\Ý[WÜ›Û\‹ˆ™]ÏY˜ZÙWÜ›Û\ ˆ -K]Ú -ˆœÝ\\›Y[[ÜžWÛÜ[˜ZK›ZY]Ø\™K˜YÛY[[ÜžWÝÛÛ‹ˆ™]ÏY˜ZÙWØYÛY[[ÜžKˆ -KØ\›š[™Ü˘Ø]ÚÝØ\›š[™Üʈ™XÛÜ™U�YBˆ -H\ÈØ]YÚ‚ˆØ\›š[™ÜËœÚ[\Yš[\Š˜[Ø^\È‹�[�[YUØ\›š[™ÊBˆܘ\Yˆ[žHHÚ]ÜÝ\\›Y[[ÜžJˆ˜\ÙWØÛY[� ˆZY]Ø\™WÛÜ[ÛœÊ�[˜[� \™X[‹YÛY[[ÜžOH˜[Ø^\ÈŠKˆ -BˆÜ™X]HH™X[Ø\Þ[˜×ØÛÛ\][Û—ØÜ™X]Jܘ\Y ] -BˆÝØ\™ÜÈHˆ›[Ù[Žˆ™Ü ]\Ý‹ˆ›Y\ÜØYÙ\ÈŽˆÞÈœ›ÛHŽˆ�\Ù\ˆ‹˜ÛÛ�[�Žˆœš]˜]HY\ÜØYÙHŸWKˆB‚ˆYˆ]OH››Ü›X[Ž‚ˆ™\ÜÛœÙHH]ØZ]Ü™X]J -ŠšÝØ\™ÜÊBˆ\ÜÙ\�™\ÜÛœÙKšYOH˜Ú]Û\ ]\Ý‚ˆ[Yˆ] ™[™ÝÚ] -‹œ˜]ÈŠN‚ˆ˜]×Ü™\ÜÛœÙHH]ØZ]Ü™X]J -ŠšÝØ\™ÜÊBˆ\ÜÙ\�˜]×Ü™\ÜÛœÙKœ\œÙJ -KšYOH˜Ú]Û\ ]\Ý‚ˆ[ÙN‚ˆ™\ÜÛœÙWØÛÛ�^HÜ™X]J -ŠšÝØ\™ÜÊBˆ\ÜÙ\�›Ý[œÜXÝ š\Ø]ØZ]X›J™\ÜÛœÙWØÛÛ�^ -Bˆ\Þ[˜ÈÚ]™\ÜÛœÙWØÛÛ�^\ÈÝ™X[Z[™×Ü™\ÜÛœÙN‚ˆ\ÜÙ\�Ý™X[Z[™×Ü™\ÜÛœÙKœÝ]\רÛÙHOH Œ ‚ˆ]ØZ]ܘ\Y �ØZ]ٛܗؘXÚÙÜ›Ý[™Ý\ÚÜÊ -Bˆ]ØZ]\Þ[˜Ú[ËœÛY\ - -BˆØË˜ÛÛXÝ + def create_sync_with_memory( + **kwargs: Any, + ) -> Any: + return self._create_with_memory_sync(original_create, **kwargs) -B‚ˆ�[�[YWÝØ\›š[™ÜÈHˆØ\›š[™Âˆ›ÜˆØ\›š[™È[ˆØ]YÚˆYˆ\ÜÝX˜Û\ÜÊØ\›š[™Ë˜Ø]YÛÜžK�[�[YUØ\›š[™ÊBˆB‚ˆ\ÜÙ\�ÛÚÝ\ÈOHÈ�[˜[� \™X[—Bˆ\ÜÙ\�Üš]\ÈOHˆ -ˆ�[˜[� \™X[‹ˆ˜ÛÛ�™\œØ][ÛŽ�™XY ][˜[� \™X[‹ˆ•\Ù\Žˆš]˜]HY\ÜØYÙH‹ˆ -BˆBˆ\ÜÙ\�[ŠÙ[�ÛY\ÜØYÙ\ÊHOH Bˆ\ÜÙ\�Ù[�ÛY\ÜØYÙ\ÖÌVÌVȘÛÛ�[�—HOHœÙXÜ™] ][˜[� \™X[‚ˆ\ÜÙ\��[�[YWÝØ\›š[™ÜÈOH×Bˆš[˜[N‚ˆ]ØZ]˜\ÙWØÛY[� ˜ÛÜÙJ -B‚‚�]\Ý ›X\šË˜\Þ[˜Ú[ÈÈ\NˆYۛܙVÝ[�\Y YXÛܘ]Ü—B˜\Þ[˜ÈYˆ\ÝÜÚ\™YØ\Þ[˜×ØÛY[�ÜØ]™\×ÛÛ›WٛܗÜÙ[XÝYÝ[˜[� - -H Oˆ›Û™N‚ˆ˜\ÙWØÛY[� ÜšYÚ[˜[ØÜ™X]HHÜ™X]WØ\Þ[˜×ØÛY[� - -BˆÛÚÝ\Έ\ÝÜÝ—HH×BˆÜš]\Έ\ÝÝ\VÜÝ‹Ü[Û˜[ÜÝ—KÝ—WHH×B‚ˆ\Þ[˜ÈYˆ˜ZÙWÜ›Û\ -ˆY\ÜØYÙ\Έ\ÝÐ[žWKˆÛÛ�Z[™\—ÝYΈÝ‹ˆÙÙÙ\Žˆ[žKˆ[ÙNˆ[žKˆ\WÚÙ^NˆÝ‹ˆ -H Oˆ\ÝÐ[žWN‚ˆÛÚÝ\˘\[™ -ÛÛ�Z[™\—ÝYÊBˆ™]\›ˆˆÈœ›ÛHŽˆœÞ\Ý[H‹˜ÛÛ�[�ŽˆˆœÙXÜ™] ^ØÛÛ�Z[™\—ÝYßHŸKˆ -›Y\ÜØYÙ\ˈB‚ˆ\Þ[˜ÈYˆ˜ZÙWØYÛY[[ÜžJˆÛY[�ˆ[žKˆÛÛ�Z[™\—ÝYΈÝ‹ˆÛÛ�[�ˆÝ‹ˆÝ\ÝÛWÚYˆÜ[Û˜[ÜÝ—KˆÙÙÙ\Žˆ[žKˆ -H Oˆ›Û™N‚ˆÜš]\˘\[™ - -ÛÛ�Z[™\—ÝYËÝ\ÝÛWÚY ÛÛ�[� -JB‚ˆÚ]]Ú -ˆœÝ\\›Y[[ÜžWÛÜ[˜ZK›ZY]Ø\™KœÝ\\›Y[[ÜžK”Ý\\›Y[[ÜžH‹ˆ™]\›—ݘ[YOS[ØÚÊ -Kˆ -K]Ú -ˆœÝ\\›Y[[ÜžWÛÜ[˜ZK›ZY]Ø\™K˜YÜÞ\Ý[WÜ›Û\‹ˆÚYWÙY™™XÝY˜ZÙWÜ›Û\ ˆ -K]Ú -ˆœÝ\\›Y[[ÜžWÛÜ[˜ZK›ZY]Ø\™K˜YÛY[[ÜžWÝÛÛ‹ˆÚYWÙY™™XÝY˜ZÙWØYÛY[[ÜžKˆ -N‚ˆ[˜[�ØNˆ[žHHÚ]ÜÝ\\›Y[[ÜžJˆ˜\ÙWØÛY[� ˆZY]Ø\™WÛÜ[ÛœÊ�[˜[� XH‹YÛY[[ÜžOH˜[Ø^\ÈŠKˆ -Bˆ[˜[�ØŽˆ[žHHÚ]ÜÝ\\›Y[[ÜžJˆ˜\ÙWØÛY[� ˆZY]Ø\™WÛÜ[ÛœÊ�[˜[� Xˆ‹YÛY[[ÜžOH˜[Ø^\ÈŠKˆ -B‚ˆ]ØZ][˜[�Ø‹˜Ú] ˜ÛÛ\][ۜ˘ܙX]Jˆ[Ù[H™Ü ]\Ý‹ˆY\ÜØYÙ\ÏVÞÈœ›ÛHŽˆ�\Ù\ˆ‹˜ÛÛ�[�Žˆœš]˜]H[˜[�ˆY\ÜØYÙHŸWKˆ -Bˆ]ØZ][˜[�ØK�ØZ]ٛܗؘXÚÙÜ›Ý[™Ý\ÚÜÊ -Bˆ]ØZ][˜[�Ø‹�ØZ]ٛܗؘXÚÙÜ›Ý[™Ý\ÚÜÊ -B‚ˆ\ÜÙ\�˜\ÙWØÛY[� ˜Ú] ˜ÛÛ\][ۜ˘ܙX]H\ÈÜšYÚ[˜[ØÜ™X]Bˆ\ÜÙ\�ÛÚÝ\ÈOHÈ�[˜[� Xˆ—Bˆ\ÜÙ\�Üš]\ÈOHˆ -ˆ�[˜[� Xˆ‹ˆ˜ÛÛ�™\œØ][ÛŽ�™XY ][˜[� Xˆ‹ˆ•\Ù\Žˆš]˜]H[˜[�ˆY\ÜØYÙH‹ˆ -BˆBˆ\ÜÙ\�ÜšYÚ[˜[ØÜ™X]K˜Ø[ØÛÝ[�OH B \ No newline at end of file + return create_sync_with_memory + + async def _create_with_memory_async( + self, + original_create: Any, + **kwargs: Any, + ) -> Any: + """Async version of create with memory injection.""" + messages = kwargs.get("messages", []) + + if self._options.add_memory == "always": + user_message = get_last_user_message(messages) + if user_message and user_message.strip(): + content = ( + get_conversation_content(messages) + if self._options.custom_id + else user_message + ) + custom_id = ( + f"conversation:{self._options.custom_id}" + if self._options.custom_id + else None + ) + + # Create background task for memory storage + task = asyncio.create_task( + add_memory_tool( + self._supermemory_client, + self._container_tag, + content, + custom_id, + self._logger, + ) + ) + + # Track the task and set up cleanup + self._background_tasks.add(task) + task.add_done_callback(self._background_tasks.discard) + + # Log any exceptions but don't fail the main request + def handle_task_exception(task_obj): + try: + if task_obj.exception() is not None: + exception = task_obj.exception() + if isinstance( + exception, + (SupermemoryNetworkError, SupermemoryAPIError), + ): + self._logger.warn( + "Background memory storage failed", + { + "error": str(exception), + "type": type(exception).__name__, + }, + ) + else: + self._logger.error( + "Unexpected error in background memory storage", + { + "error": str(exception), + "type": type(exception).__name__, + }, + ) + except asyncio.CancelledError: + self._logger.debug("Memory storage task was cancelled") + + task.add_done_callback(handle_task_exception) + + if self._options.mode != "profile": + user_message = get_last_user_message(messages) + if not user_message: + self._logger.debug("No user message found, skipping memory search") + return await original_create(**kwargs) + + self._logger.info( + "Starting memory search", + { + "container_tag": self._container_tag, + "conversation_id": self._options.custom_id, + "mode": self._options.mode, + }, + ) + + enhanced_messages = await add_system_prompt( + messages, + self._container_tag, + self._logger, + self._options.mode, + self._get_api_key(), + ) + + kwargs["messages"] = enhanced_messages + return await original_create(**kwargs) + + def _create_with_memory_sync( + self, + original_create: Any, + **kwargs: Any, + ) -> Any: + """Sync version of create with memory injection.""" + # For sync clients, we implement a simplified version without background tasks + messages = kwargs.get("messages", []) + + # Handle memory addition synchronously if needed + if self._options.add_memory == "always": + user_message = get_last_user_message(messages) + if user_message and user_message.strip(): + content = ( + get_conversation_content(messages) + if self._options.custom_id + else user_message + ) + custom_id = ( + f"conversation:{self._options.custom_id}" + if self._options.custom_id + else None + ) + + # Use asyncio.run() for the memory addition + try: + asyncio.run( + add_memory_tool( + self._supermemory_client, + self._container_tag, + content, + custom_id, + self._logger, + ) + ) + except RuntimeError as e: + if "cannot be called from a running event loop" in str(e): + # We're in an async context, log warning and skip memory saving + self._logger.warn( + "Cannot save memory in sync client from async context", + {"error": str(e)}, + ) + else: + raise + except SupermemoryNetworkError as e: + # Network errors are expected, log as warning + self._logger.warn("Network error saving memory", {"error": str(e)}) + except (SupermemoryAPIError, SupermemoryMemoryOperationError) as e: + # API/memory errors are concerning, log as error + self._logger.error("Failed to save memory", {"error": str(e)}) + except Exception as e: + # Unexpected errors should be investigated + self._logger.error( + "Unexpected error saving memory", + {"error": str(e), "type": type(e).__name__}, + ) + + # Handle memory search and injection + if self._options.mode != "profile": + user_message = get_last_user_message(messages) + if not user_message: + self._logger.debug("No user message found, skipping memory search") + return original_create(**kwargs) + + self._logger.info( + "Starting memory search", + { + "container_tag": self._container_tag, + "conversation_id": self._options.custom_id, + "mode": self._options.mode, + }, + ) + + # Use asyncio.run() for memory search and injection + try: + enhanced_messages = asyncio.run( + add_system_prompt( + messages, + self._container_tag, + self._logger, + self._options.mode, + self._get_api_key(), + ) + ) + except RuntimeError as e: + if "cannot be called from a running event loop" in str(e): + # We're in an async context, run in a separate thread + import concurrent.futures + + with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: + future = executor.submit( + asyncio.run, + add_system_prompt( + messages, + self._container_tag, + self._logger, + self._options.mode, + self._get_api_key(), + ), + ) + enhanced_messages = future.result() + else: + raise + + kwargs["messages"] = enhanced_messages + return original_create(**kwargs) + + async def wait_for_background_tasks(self, timeout: Optional[float] = 10.0) -> None: + """ + Wait for all background memory storage tasks to complete. + + Args: + timeout: Maximum time to wait in seconds. None for no timeout. + + Raises: + asyncio.TimeoutError: If tasks don't complete within timeout + """ + if not self._background_tasks: + return + + self._logger.debug( + f"Waiting for {len(self._background_tasks)} background tasks to complete" + ) + + try: + if timeout is not None: + await asyncio.wait_for( + asyncio.gather(*self._background_tasks, return_exceptions=True), + timeout=timeout, + ) + else: + await asyncio.gather(*self._background_tasks, return_exceptions=True) + + self._logger.debug("All background tasks completed") + except asyncio.TimeoutError: + self._logger.warn( + f"Background tasks did not complete within {timeout}s timeout" + ) + # Cancel remaining tasks + tasks_to_cancel = [ + task for task in self._background_tasks if not task.done() + ] + for task in tasks_to_cancel: + task.cancel() + + if tasks_to_cancel: + await asyncio.gather(*tasks_to_cancel, return_exceptions=True) + raise + + def cancel_background_tasks(self) -> None: + """Cancel all pending background tasks.""" + cancelled_count = 0 + for task in self._background_tasks: + if not task.done(): + task.cancel() + cancelled_count += 1 + + if cancelled_count > 0: + self._logger.debug(f"Cancelled {cancelled_count} pending background tasks") + + async def __aenter__(self): + """Async context manager entry.""" + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Async context manager exit - wait for background tasks.""" + try: + await self.wait_for_background_tasks(timeout=5.0) + except asyncio.TimeoutError: + self._logger.warn("Some background memory tasks did not complete on exit") + + def __enter__(self): + """Sync context manager entry.""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Sync context manager exit - attempt to wait for background tasks.""" + if self._background_tasks: + try: + # Try to wait for background tasks in sync context + asyncio.run(self.wait_for_background_tasks(timeout=5.0)) + except RuntimeError as e: + if "cannot be called from a running event loop" in str(e): + # In async context, just cancel the tasks + self._logger.warn( + "Cannot wait for background tasks in sync context from async environment. " + "Use async context manager or call wait_for_background_tasks() manually." + ) + self.cancel_background_tasks() + else: + raise + except asyncio.TimeoutError: + self._logger.warn( + "Some background memory tasks did not complete on exit" + ) + self.cancel_background_tasks() + + def __getattr__(self, name: str) -> Any: + """Delegate all other attributes to the wrapped client.""" + if name in {"with_raw_response", "with_streaming_response"}: + value = self._create_client_variant_facade(name) + setattr(self, name, value) + return value + return getattr(self._client, name) + + +def with_supermemory( + openai_client: Union[OpenAI, AsyncOpenAI], + options: OpenAIMiddlewareOptions, +) -> Union[OpenAI, AsyncOpenAI]: + """ + Wraps an OpenAI client with SuperMemory middleware to automatically inject relevant memories + into the system prompt based on the user's message content. + + This middleware searches the supermemory API for relevant memories using the container tag + and user message, then either appends memories to an existing system prompt or creates + a new system prompt with the memories. + + Args: + openai_client: The OpenAI client to wrap with SuperMemory middleware + options: Configuration options for the middleware (container_tag and custom_id are required) + + Returns: + An OpenAI client with SuperMemory middleware injected + + Example: + ```python + from supermemory_openai import with_supermemory, OpenAIMiddlewareOptions + from openai import OpenAI + + # Create OpenAI client with supermemory middleware + openai = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) + openai_with_supermemory = with_supermemory( + openai, + OpenAIMiddlewareOptions( + container_tag="user-123", + custom_id="conversation-456", + mode="full", + add_memory="always" + ) + ) + + # Use normally - memories will be automatically injected + response = await openai_with_supermemory.chat.completions.create( + model="gpt-4", + messages=[ + {"role": "user", "content": "What's my favorite programming language?"} + ] + ) + ``` + + Raises: + ValueError: When SUPERMEMORY_API_KEY environment variable is not set + Exception: When supermemory API request fails + """ + wrapper = SupermemoryOpenAIWrapper(openai_client, options) + # Return the wrapper, which delegates all attributes to the original client + return cast(Union[OpenAI, AsyncOpenAI], wrapper) diff --git a/packages/openai-sdk-python/tests/test_client_isolation.py b/packages/openai-sdk-python/tests/test_client_isolation.py index 700ac4cf..a0a07ace 100644 --- a/packages/openai-sdk-python/tests/test_client_isolation.py +++ b/packages/openai-sdk-python/tests/test_client_isolation.py @@ -3,12 +3,18 @@ from __future__ import annotations import asyncio +import gc +import inspect +import json import os +import warnings from types import SimpleNamespace from typing import Any, Generator, Literal, Optional from unittest.mock import AsyncMock, Mock, patch +import httpx import pytest +from openai import AsyncOpenAI from supermemory_openai import OpenAIMiddlewareOptions, with_supermemory @@ -113,6 +119,35 @@ def response_variant_creates(client: Any, name: str) -> list[Any]: ] +REAL_ASYNC_COMPLETION_PATHS = ( + "normal", + "completions.raw", + "chat.raw", + "client.raw", + "completions.streaming", + "chat.streaming", + "client.streaming", +) + + +def real_async_completion_create(client: Any, path: str) -> Any: + if path == "normal": + return client.chat.completions.create + if path == "completions.raw": + return client.chat.completions.with_raw_response.create + if path == "chat.raw": + return client.chat.with_raw_response.completions.create + if path == "client.raw": + return client.with_raw_response.chat.completions.create + if path == "completions.streaming": + return client.chat.completions.with_streaming_response.create + if path == "chat.streaming": + return client.chat.with_streaming_response.completions.create + if path == "client.streaming": + return client.with_streaming_response.chat.completions.create + raise AssertionError(f"Unknown completion path: {path}") + + @pytest.fixture(autouse=True) # type: ignore[untyped-decorator] def supermemory_api_key() -> Generator[None, None, None]: with patch.dict(os.environ, {"SUPERMEMORY_API_KEY": "test-key"}): @@ -330,6 +365,121 @@ def test_async_raw_and_streaming_response_prefixes_remain_memory_aware() -> None assert "secret-tenant-b" in str(create.call_args.kwargs["messages"]) +@pytest.mark.parametrize("path", REAL_ASYNC_COMPLETION_PATHS) +@pytest.mark.asyncio +async def test_real_async_openai_paths_use_async_middleware(path: str) -> None: + lookups: list[str] = [] + writes: list[tuple[str, Optional[str], str]] = [] + sent_messages: list[list[Any]] = [] + + async def fake_prompt( + messages: list[Any], + container_tag: str, + logger: Any, + mode: Any, + api_key: str, + ) -> list[Any]: + lookups.append(container_tag) + return [ + {"role": "system", "content": f"secret-{container_tag}"}, + *messages, + ] + + async def fake_add_memory( + client: Any, + container_tag: str, + content: str, + custom_id: Optional[str], + logger: Any, + ) -> None: + writes.append((container_tag, custom_id, content)) + + def handle_request(request: httpx.Request) -> httpx.Response: + body = json.loads(request.content) + sent_messages.append(body["messages"]) + return httpx.Response( + 200, + request=request, + headers={"content-type": "application/json"}, + json={ + "id": "chatcmpl-test", + "object": "chat.completion", + "created": 0, + "model": "gpt-test", + "choices": [ + { + "index": 0, + "message": {"role": "assistant", "content": "ok"}, + "finish_reason": "stop", + } + ], + }, + ) + + http_client = httpx.AsyncClient(transport=httpx.MockTransport(handle_request)) + base_client = AsyncOpenAI(api_key="openai-test", http_client=http_client) + + try: + with patch( + "supermemory_openai.middleware.supermemory.Supermemory", + return_value=Mock(), + ), patch( + "supermemory_openai.middleware.add_system_prompt", + new=fake_prompt, + ), patch( + "supermemory_openai.middleware.add_memory_tool", + new=fake_add_memory, + ), warnings.catch_warnings( + record=True + ) as caught: + warnings.simplefilter("always", RuntimeWarning) + wrapped: Any = with_supermemory( + base_client, + middleware_options("tenant-real", add_memory="always"), + ) + create = real_async_completion_create(wrapped, path) + kwargs = { + "model": "gpt-test", + "messages": [{"role": "user", "content": "private message"}], + } + + if path == "normal": + response = await create(**kwargs) + assert response.id == "chatcmpl-test" + elif path.endswith(".raw"): + raw_response = await create(**kwargs) + assert raw_response.parse().id == "chatcmpl-test" + else: + response_context = create(**kwargs) + assert not inspect.isawaitable(response_context) + async with response_context as streaming_response: + assert streaming_response.status_code == 200 + + await wrapped.wait_for_background_tasks() + await asyncio.sleep(0) + gc.collect() + + runtime_warnings = [ + warning + for warning in caught + if issubclass(warning.category, RuntimeWarning) + ] + + assert lookups == ["tenant-real"] + assert writes == [ + ( + "tenant-real", + "conversation:thread-tenant-real", + "User: private message", + ) + ] + assert len(sent_messages) == 1 + assert sent_messages[0][0]["content"] == "secret-tenant-real" + assert runtime_warnings == [] + finally: + await base_client.close() + + @pytest.mark.asyncio # type: ignore[untyped-decorator] async def test_shared_async_client_saves_only_for_selected_tenant() -> None: base_client, original_create = create_async_client()