fix: cache list_all_params() to prevent RecursionError (fixes #25859)

Pre-compute DynamicPromptManagementParamLiteral values as a module-level
frozenset (DYNAMIC_PROMPT_MANAGEMENT_PARAMS) so the call site uses O(1)
membership checks instead of rebuilding a list on every loop iteration.

This follows the established pattern used by LIST_BATCHES_SUPPORTED_PROVIDERS
(types/utils.py line 3347) and _LITELLM_ROUTES_MEMBER_NAMES (route_checks.py).

No import cycle: DYNAMIC_PROMPT_MANAGEMENT_PARAMS is defined in types/utils.py
which litellm_logging.py already imports from (line 100). Adding one symbol to
an existing import block cannot create a circular dependency.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Yiyang Liu <yiyangliu@microsoft.com>
This commit is contained in:
Yiyang Liu 2026-04-29 18:25:51 -07:00
parent 3e1479c052
commit 5da3edc98b
4 changed files with 71 additions and 1 deletions

View file

@ -97,6 +97,7 @@ from litellm.types.utils import (
CostBreakdown,
CostResponseTypes,
CustomPricingLiteLLMParams,
DYNAMIC_PROMPT_MANAGEMENT_PARAMS,
DynamicPromptManagementParamLiteral,
EmbeddingResponse,
GuardrailStatus,
@ -684,7 +685,7 @@ class Logging(LiteLLMLoggingBaseClass):
eg. AnthropicCacheControlHook and BedrockKnowledgeBaseHook both don't require a `prompt_id` to be passed in, they are triggered by dynamic params
"""
for param in non_default_params:
if param in DynamicPromptManagementParamLiteral.list_all_params():
if param in DYNAMIC_PROMPT_MANAGEMENT_PARAMS:
return True
#############################################################################

View file

@ -3601,6 +3601,11 @@ class DynamicPromptManagementParamLiteral(str, Enum):
return [param.value for param in cls]
DYNAMIC_PROMPT_MANAGEMENT_PARAMS: frozenset[str] = frozenset(
param.value for param in DynamicPromptManagementParamLiteral
)
class CallbacksByType(TypedDict):
success: List[str]
failure: List[str]

View file

@ -36,6 +36,49 @@ def test_get_masked_api_base(logging_obj):
assert type(masked_api_base) == str
@pytest.mark.parametrize(
"non_default_params",
[
{"cache_control_injection_points": [{"location": "message", "index": -1}]},
{"knowledge_bases": ["kb-1"]},
{"vector_store_ids": ["vs-1"]},
],
)
def test_should_run_prompt_management_hooks_without_prompt_id_for_dynamic_params(
logging_obj, non_default_params
):
assert (
logging_obj.should_run_prompt_management_hooks(
non_default_params=non_default_params,
prompt_id=None,
tools=None,
)
is True
)
@pytest.mark.parametrize(
"non_default_params",
[
{"temperature": 0.5},
{"max_tokens": 100},
{"temperature": 0.7, "top_p": 0.9, "max_tokens": 256},
{},
],
)
def test_should_run_prompt_management_hooks_false_for_non_dynamic_params(
logging_obj, non_default_params
):
assert (
logging_obj.should_run_prompt_management_hooks(
non_default_params=non_default_params,
prompt_id=None,
tools=None,
)
is False
)
def test_sentry_sample_rate():
existing_sample_rate = os.getenv("SENTRY_API_SAMPLE_RATE")
try:

View file

@ -19,6 +19,27 @@ def test_hidden_params_response_ms():
assert hidden_params_dict.get("_response_ms") == 100
def test_dynamic_prompt_management_list_all_params_preserves_enum_order():
from litellm.types.utils import DynamicPromptManagementParamLiteral
assert DynamicPromptManagementParamLiteral.list_all_params() == [
"cache_control_injection_points",
"knowledge_bases",
"vector_store_ids",
]
def test_dynamic_prompt_management_params_frozenset():
from litellm.types.utils import DYNAMIC_PROMPT_MANAGEMENT_PARAMS
assert "cache_control_injection_points" in DYNAMIC_PROMPT_MANAGEMENT_PARAMS
assert "knowledge_bases" in DYNAMIC_PROMPT_MANAGEMENT_PARAMS
assert "vector_store_ids" in DYNAMIC_PROMPT_MANAGEMENT_PARAMS
assert "temperature" not in DYNAMIC_PROMPT_MANAGEMENT_PARAMS
assert "max_tokens" not in DYNAMIC_PROMPT_MANAGEMENT_PARAMS
assert isinstance(DYNAMIC_PROMPT_MANAGEMENT_PARAMS, frozenset)
def test_chat_completion_delta_tool_call():
from litellm.types.utils import ChatCompletionDeltaToolCall, Function