litellm - ✅(Solved) Fix RecursionError: DynamicPromptManagementParamLiteral.list_all_params() uncached, causes stack exhaustion in deep async call stacks (agent frameworks) [1 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
BerriAI/litellm#25859Fetched 2026-04-17 08:28:37
View on GitHub
Comments
0
Participants
1
Timeline
4
Reactions
0
Author
Participants
Timeline (top)
cross-referenced ×1mentioned ×1referenced ×1unsubscribed ×1

Error Message

File "litellm/litellm_core_utils/litellm_logging.py", line 542, in should_run_prompt_management_hooks if self._should_run_prompt_management_hooks_without_prompt_id( File "litellm/litellm_core_utils/litellm_logging.py", line 561, in _should_run_prompt_management_hooks_without_prompt_id if param in DynamicPromptManagementParamLiteral.list_all_params(): File "litellm/types/utils.py", line 2866, in list_all_params return [param.value for param in cls] RecursionError: maximum recursion depth exceeded

Root Cause

The immediate trigger is DynamicPromptManagementParamLiteral.list_all_params() in litellm_core_utils/litellm_logging.py, but the root cause is that this method is called O(n) times per request (once per kwarg key in non_default_params) and creates a new list on every call:

Fix Action

Workaround

Dispatch LiteLLM calls via asyncio.create_task so the coroutine starts from the event loop with a fresh stack frame counter:

response = await asyncio.create_task(litellm.acompletion(**kwargs))

PR fix notes

PR #25903: fix: cache list_all_params() to prevent RecursionError

Description (problem / solution / changelog)

What's broken?

DynamicPromptManagementParamLiteral.list_all_params() creates a new list on every call without caching. In deep async frameworks (Google ADK, LangGraph, CrewAI with 70+ sub-agents), the call stack is already near the recursion limit (~4000+ frames). Each acompletion call invokes list_all_params() once per kwarg key in non_default_params, and these extra frames push the stack over the limit, causing RecursionError.

Who is affected?

Users running LiteLLM inside agent orchestration frameworks with deep async call stacks. Standard direct usage is not affected.

Root Cause

_should_run_prompt_management_hooks_without_prompt_id iterates over non_default_params and calls DynamicPromptManagementParamLiteral.list_all_params() on every iteration, which rebuilds a list each time via [param.value for param in cls]. This is both wasteful (O(n) list creation per check) and dangerous (adds stack frames that can overflow).

Fix

Pre-compute the enum values as a module-level frozenset (_DYNAMIC_PROMPT_MANAGEMENT_ALL_PARAMS) at import time. The call site now checks membership against this frozenset directly, giving:

  • Zero per-call allocation — the frozenset is built once at module load
  • O(1) membership checks — frozenset vs O(n) list scan
  • No extra stack frames — eliminates the list_all_params() call from the hot path

list_all_params() is preserved for backward compatibility but now returns from the pre-computed frozenset.

Testing

  • Both modified files pass py_compile syntax verification
  • The cached result matches the original computation (same 3 values: cache_control_injection_points, knowledge_bases, vector_store_ids)

Fixes #25859

Changed files

  • litellm/litellm_core_utils/litellm_logging.py (modified, +2/-1)
  • litellm/types/utils.py (modified, +6/-1)

Code Example

# litellm/types/utils.py:2866
@classmethod
def list_all_params(cls):
    return [param.value for param in cls]  # new list, every call, no caching

---

# litellm_core_utils/litellm_logging.py:561
for param in non_default_params:
    if param in DynamicPromptManagementParamLiteral.list_all_params():  # called once per key
        return True

---

File "litellm/litellm_core_utils/litellm_logging.py", line 542, in should_run_prompt_management_hooks
    if self._should_run_prompt_management_hooks_without_prompt_id(
File "litellm/litellm_core_utils/litellm_logging.py", line 561, in _should_run_prompt_management_hooks_without_prompt_id
    if param in DynamicPromptManagementParamLiteral.list_all_params():
File "litellm/types/utils.py", line 2866, in list_all_params
    return [param.value for param in cls]
RecursionError: maximum recursion depth exceeded

---

from functools import lru_cache

class DynamicPromptManagementParamLiteral(str, Enum):
    @classmethod
    @lru_cache(maxsize=None)
    def list_all_params(cls) -> list[str]:
        return [param.value for param in cls]

---

@classmethod
    @lru_cache(maxsize=None)
    def as_set(cls) -> frozenset[str]:
        return frozenset(param.value for param in cls)

# call site:
if param in DynamicPromptManagementParamLiteral.as_set():

---

_DYNAMIC_PROMPT_PARAMS: frozenset[str] = frozenset(
    p.value for p in DynamicPromptManagementParamLiteral
)

---

response = await asyncio.create_task(litellm.acompletion(**kwargs))
RAW_BUFFERClick to expand / collapse

Bug Report

LiteLLM version: 1.80.0 Python: 3.13

Problem

When using LiteLLM inside a deep async call stack (e.g. Google ADK with orchestrator → sub-agents → tools nesting), acompletion consistently raises RecursionError: maximum recursion depth exceeded.

The immediate trigger is DynamicPromptManagementParamLiteral.list_all_params() in litellm_core_utils/litellm_logging.py, but the root cause is that this method is called O(n) times per request (once per kwarg key in non_default_params) and creates a new list on every call:

# litellm/types/utils.py:2866
@classmethod
def list_all_params(cls):
    return [param.value for param in cls]  # new list, every call, no caching

This is invoked from _should_run_prompt_management_hooks_without_prompt_id:

# litellm_core_utils/litellm_logging.py:561
for param in non_default_params:
    if param in DynamicPromptManagementParamLiteral.list_all_params():  # called once per key
        return True

Impact

In agent orchestration frameworks (Google ADK, LangGraph, CrewAI) where a root agent delegates to sub-agents which call tools that invoke LiteLLM, the async call stack is already deep (~4000+ frames with setrecursionlimit(5000)) before acompletion is called. list_all_params() is then the final call that pushes the stack over the limit.

Cascading failure: Once the first RecursionError fires, LiteLLM's fallback mechanism tries each fallback model — but the stack hasn't fully unwound, so every fallback attempt immediately re-hits the limit, producing dozens of nested RecursionError tracebacks that obscure the real root cause.

Traceback (trimmed)

File "litellm/litellm_core_utils/litellm_logging.py", line 542, in should_run_prompt_management_hooks
    if self._should_run_prompt_management_hooks_without_prompt_id(
File "litellm/litellm_core_utils/litellm_logging.py", line 561, in _should_run_prompt_management_hooks_without_prompt_id
    if param in DynamicPromptManagementParamLiteral.list_all_params():
File "litellm/types/utils.py", line 2866, in list_all_params
    return [param.value for param in cls]
RecursionError: maximum recursion depth exceeded

Suggested Fix

1. Cache list_all_params (prevents redundant allocations + reduces frame count):

from functools import lru_cache

class DynamicPromptManagementParamLiteral(str, Enum):
    @classmethod
    @lru_cache(maxsize=None)
    def list_all_params(cls) -> list[str]:
        return [param.value for param in cls]

2. Use a frozenset for O(1) membership check (better than list):

    @classmethod
    @lru_cache(maxsize=None)
    def as_set(cls) -> frozenset[str]:
        return frozenset(param.value for param in cls)

# call site:
if param in DynamicPromptManagementParamLiteral.as_set():

3. Pre-compute at class definition time (zero runtime overhead):

_DYNAMIC_PROMPT_PARAMS: frozenset[str] = frozenset(
    p.value for p in DynamicPromptManagementParamLiteral
)

Workaround

Dispatch LiteLLM calls via asyncio.create_task so the coroutine starts from the event loop with a fresh stack frame counter:

response = await asyncio.create_task(litellm.acompletion(**kwargs))

Environment

  • LiteLLM 1.80.0
  • Python 3.13 / uvloop
  • Google ADK agent framework with 70+ sub-agents
  • sys.setrecursionlimit(5000)

extent analysis

TL;DR

Cache the result of DynamicPromptManagementParamLiteral.list_all_params() to prevent redundant allocations and reduce frame count.

Guidance

  • Implement caching using functools.lru_cache as suggested in the issue to prevent redundant allocations and reduce frame count.
  • Consider using a frozenset for O(1) membership check instead of a list for better performance.
  • Pre-computing the set of parameters at class definition time can also eliminate runtime overhead.
  • As a temporary workaround, dispatching LiteLLM calls via asyncio.create_task can help mitigate the recursion error by starting the coroutine with a fresh stack frame counter.

Example

from functools import lru_cache

class DynamicPromptManagementParamLiteral(str, Enum):
    @classmethod
    @lru_cache(maxsize=None)
    def list_all_params(cls) -> list[str]:
        return [param.value for param in cls]

Notes

The provided suggestions are based on the information given in the issue and may not be exhaustive. The effectiveness of these suggestions may vary depending on the specific use case and environment.

Recommendation

Apply the suggested fix by caching the result of DynamicPromptManagementParamLiteral.list_all_params() to prevent redundant allocations and reduce frame count, as it directly addresses the root cause of the issue.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING