#10345·dspy

[Bug] `dspy.LM(stream=True)` caches LiteLLM's live stream before rejecting it

Author: amitxyCreated Sep 6, 2026Updated Sep 11, 2026
Labelsbug

What happened?

Supplying LiteLLM's stream=True kwarg to dspy.LM returns a live LiteLLM CustomStreamWrapper, but DSPy subsequently handles it as a completed response. The first call fails with:

TypeError: 'CustomStreamWrapper' object is not subscriptable

With cache=True, DSPy stores the live wrapper before LM.forward() fails while processing it. On a subsequent identical request, DSPy finds the cached wrapper and attempts to deep-copy it. This fails before the provider is called:

LMUnexpectedError: [<configured model>] cannot pickle '_thread.RLock' object

This exposes two coupled problems: LM.forward() cannot handle the live wrapper returned by raw stream=True, and DSPy caches that unsupported object before validating it. The cached wrapper then causes the subsequent identical request to fail during cache retrieval.

Steps to reproduce

import os

import dspy
from dotenv import load_dotenv
load_dotenv()

# Use a fresh in-memory cache so call 1 misses and call 2 hits.
dspy.configure_cache(enable_disk_cache=False, enable_memory_cache=True)
dspy.cache.reset_memory_cache()

lm = dspy.LM(
    model=os.getenv("TEST_MODEL", "openai/qwen3.6-35b-fast"),
    api_key=os.getenv("PROVIDER_API_KEY") or os.getenv("OPENAI_API_KEY"),
    api_base=os.getenv("PROVIDER_API_BASE") or os.getenv("OPENAI_BASE_URL"),
    stream=True,
    cache=True,
)
prompt = "Return the single word: SUCCESS"

try:
    lm(prompt)
except TypeError as exc:
    print(f"Call 1 (cache miss): {exc}")

print("Call 2: identical request, cache hit")
lm(prompt)   # Intentionally uncaught: raises LMUnexpectedError from cache deepcopy.

Observed output:

Call 1 (cache miss): 'CustomStreamWrapper' object is not subscriptable
Call 2: identical request, cache hit
Traceback (most recent call last):
...
LMUnexpectedError: [<configured model>] cannot pickle '_thread.RLock' object

The same first-call CustomStreamWrapper error occurs with cache=False; caching adds the second, persistent failure.

Execution trace

This sequence follows directly from the tested source at commit 35ef21f1689576c4fbe06c27e22eabb5db1b24b6:

  1. dspy.LM(..., stream=True) accepts stream through **kwargs; BaseLM stores those kwargs in self.kwargs (base_lm.py:171-206).
  2. LM.forward() merges self.kwargs into the request, wraps the completion function with request_cache when cache=True, and invokes it (lm.py:232-257).
  3. With no dspy.streamify() side channel, litellm_completion() returns the direct litellm.completion(..., **request) result (lm.py:454-458, lm.py:495-510). LiteLLM documents stream=True as returning a streaming response and annotates completion() as returning ModelResponse | CustomStreamWrapper (LiteLLM source).
  4. On the cache miss, request_cache calls the completion function, stores its raw result with cache.put(...), then returns that same result (cache.py:267-286). Therefore the live CustomStreamWrapper is cached before LM.forward() sees it.
  5. Only after that return does LM.forward() call _check_truncation(results) (lm.py:253-265). _check_truncation() indexes results["choices"] (lm.py:437-445), producing the first-call not-subscriptable error.
  6. On call two, request_cache checks cache.get(...) before calling LiteLLM and returns immediately on a hit (cache.py:267-277). The memory-cache read calls copy.deepcopy(response) (cache.py:125-150); the observed RLock error is raised there and wrapped as LMUnexpectedError by LM.forward() (lm.py:258-261).

Expected behavior

DSPy should reject stream=True when it is passed directly to either dspy.LM(...) or an LM call. The validation should happen before a provider request is issued or a cached value is read or written, and the error should direct users to DSPy's supported streaming API.

I think the simplest fix would be to check for stream=True after the LM arguments are combined, but before DSPy accesses the cache or calls the provider. If this matches the intended behavior, I’d be happy to open a PR with the fix and regression tests :)

Environment

  • DSPy: main at 35ef21f1689576c4fbe06c27e22eabb5db1b24b6 (3.3.1)
  • LiteLLM: 1.99.0
  • Python: 3.12.3

DSPy version

3.3.1