-
Notifications
You must be signed in to change notification settings - Fork 422
Description
Hey folks! Longtime user via pytest-recording, first time issue-filer. Big fan of v8 because of #943 , allowed me to remove a bunch of monkeypatched conftest.py code. Unfortunately today I found myself adding some more monkeypatching back in, due to some interaction with httpx via dspy/litellm.
When using VCR with libraries that use httpx/httpcore internally (like litellm/DSPy), requests fail with RuntimeError: await wasn't used with future when there's already a running event loop.
For me, this happens when:
- Running tests with pytest (which may have an event loop)
- Making HTTP requests through a library that uses httpx internally
- VCR intercepts the request via httpcore stubs
Repro / Traceback
For example:
import dspy
@pytest.mark.vcr
def test_llm_call():
lm = dspy.LM("gemini/gemini-2.0-flash", api_key="...")
with dspy.context(lm=lm):
result = my_dspy_module(input="test") # FailsAnd the stack:
litellm.exceptions.APIConnectionError: litellm.APIConnectionError: await wasn't used with future
Traceback (most recent call last):
File "/opt/app/venv/lib/python3.11/site-packages/litellm/main.py", line 3113, in completion
response = vertex_chat_completion.completion(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/app/venv/lib/python3.11/site-packages/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py", line 2599, in completion
response = client.post(url=url, headers=headers, json=data, logging_obj=logging_obj)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/app/venv/lib/python3.11/site-packages/litellm/llms/custom_httpx/http_handler.py", line 960, in post
response = self.client.send(req, stream=stream)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/app/venv/lib/python3.11/site-packages/httpx/_client.py", line 926, in send
response = self._send_handling_auth(
^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/app/venv/lib/python3.11/site-packages/httpx/_client.py", line 1027, in _send_single_request
response = transport.handle_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/app/venv/lib/python3.11/site-packages/httpx/_transports/default.py", line 236, in handle_request
resp = self._pool.handle_request(req)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/app/venv/lib/python3.11/site-packages/vcr/stubs/httpcore_stubs.py", line 213, in _inner_handle_request
return _vcr_handle_request(cassette, real_handle_request, self, real_request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/app/venv/lib/python3.11/site-packages/vcr/stubs/httpcore_stubs.py", line 195, in _vcr_handle_request
vcr_request, vcr_response = _run_async_function(
^^^^^^^^^^^^^^^^^^^^^^^^^
RuntimeError: await wasn't used with future
I think this is because in vcr/stubs/httpcore_stubs.py, the _run_async_function does this:
vcrpy/vcr/stubs/httpcore_stubs.py
Lines 178 to 191 in ca4b1e1
| def _run_async_function(sync_func, *args, **kwargs): | |
| """ | |
| Safely run an asynchronous function from a synchronous context. | |
| Handles both cases: | |
| - An event loop is already running. | |
| - No event loop exists yet. | |
| """ | |
| try: | |
| asyncio.get_running_loop() | |
| except RuntimeError: | |
| return asyncio.run(sync_func(*args, **kwargs)) | |
| else: | |
| # If inside a running loop, create a task and wait for it | |
| return asyncio.ensure_future(sync_func(*args, **kwargs)) |
The problem is in the else branch: asyncio.ensure_future() returns a Future object, but doesn't wait for it. The caller expects the actual result, not a Future.
Note that I know this worked with v7 + an earlier version of litellm, but can't honestly say this is for sure the root cause.
Workaround
For now I just throw a thread around it (via monkeypatch in conftest.py):
import asyncio
import threading
def _patched_run_async_function(async_func, *args, **kwargs):
try:
asyncio.get_running_loop()
except RuntimeError:
return asyncio.run(async_func(*args, **kwargs))
else:
# Run in separate thread with its own event loop
result = None
exception = None
def run_in_thread():
nonlocal result, exception
try:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
result = loop.run_until_complete(async_func(*args, **kwargs))
finally:
loop.close()
asyncio.set_event_loop(None)
except Exception as e:
exception = e
thread = threading.Thread(target=run_in_thread)
thread.start()
thread.join()
if exception is not None:
raise exception
return result
# Apply patch
import vcr.stubs.httpcore_stubs as httpcore_stubs
httpcore_stubs._run_async_function = _patched_run_async_functioncc @jairhenrique / @seowalex bc #943
Thanks for all your hard work!