diff --git a/litellm/integrations/prompt_management_base.py b/litellm/integrations/prompt_management_base.py index d16afa92ec2..81c01599e77 100644 --- a/litellm/integrations/prompt_management_base.py +++ b/litellm/integrations/prompt_management_base.py @@ -165,7 +165,7 @@ class PromptManagementBase(ABC): ignore_prompt_manager_optional_params: bool | None = False, ) -> tuple[str, list[AllMessageValues], dict]: if prompt_id is None: - raise ValueError("prompt_id is required for Prompt Management Base class") + return model, messages, non_default_params if not self.should_run_prompt_management( prompt_id=prompt_id, prompt_spec=prompt_spec, diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 91a312b4f45..9b7707eabe1 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -832,8 +832,8 @@ 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(): + for param in DynamicPromptManagementParamLiteral.list_all_params(): + if non_default_params.get(param): return True ############################################################################# @@ -966,6 +966,23 @@ class Logging(LiteLLMLoggingBaseClass): return None + @staticmethod + def _prompt_manager_runs_without_prompt_id( + logger: CustomLogger, + prompt_spec: PromptSpec | None, + dynamic_callback_params: StandardCallbackDynamicParams | None, + ) -> bool: + if not isinstance(logger, CustomPromptManagement): + return False + try: + return logger.should_run_prompt_management( + prompt_id=None, + prompt_spec=prompt_spec, + dynamic_callback_params=dynamic_callback_params or StandardCallbackDynamicParams(), + ) + except Exception: + return False + def get_custom_logger_for_prompt_management( self, model: str, @@ -1016,8 +1033,13 @@ class Logging(LiteLLMLoggingBaseClass): callback_type=CustomPromptManagement ) - if prompt_management_loggers: - logger: Final = prompt_management_loggers[0] + for logger in prompt_management_loggers: + if prompt_id is None and not self._prompt_manager_runs_without_prompt_id( + logger=logger, + prompt_spec=prompt_spec, + dynamic_callback_params=dynamic_callback_params, + ): + continue self.model_call_details["prompt_integration"] = logger.__class__.__name__ return logger diff --git a/tests/test_litellm/litellm_core_utils/test_litellm_logging.py b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py index c2d73ea467d..05aa65034fb 100644 --- a/tests/test_litellm/litellm_core_utils/test_litellm_logging.py +++ b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py @@ -5105,3 +5105,81 @@ def test_set_cost_breakdown_stores_vertex_location(): cost_for_built_in_tools_cost_usd_dollar=0.0, ) assert no_location.cost_breakdown.get("vertex_location") is None + + +def test_prompt_hooks_skip_prompt_managers_when_no_prompt_id(logging_obj, tmp_path, monkeypatch): + """ + Regression for UI-injected `vector_store_ids: []` and always-on non-empty `vector_store_ids` + with a registered prompt manager (e.g. dotprompt): requests without a prompt_id 500'd with + "prompt_id is required for Prompt Management Base class" instead of completing normally. + """ + from litellm.integrations.dotprompt.dotprompt_manager import DotpromptManager + from litellm.integrations.vector_store_integrations.vector_store_pre_call_hook import ( + VectorStorePreCallHook, + ) + from litellm.types.vector_stores import LiteLLM_ManagedVectorStore + from litellm.vector_stores.vector_store_registry import VectorStoreRegistry + + (tmp_path / "stem.prompt").write_text("---\nmodel: gemini-2.5-flash\n---\nyou are a stem tutor\n") + dotprompt_manager = DotpromptManager(prompt_directory=str(tmp_path)) + litellm.logging_callback_manager.add_litellm_callback(dotprompt_manager) + monkeypatch.setattr( + litellm, + "vector_store_registry", + VectorStoreRegistry( + vector_stores=[LiteLLM_ManagedVectorStore(vector_store_id="vs_123", custom_llm_provider="openai")] + ), + ) + + messages = [{"role": "user", "content": "hi"}] + try: + assert not logging_obj.should_run_prompt_management_hooks( + prompt_id=None, non_default_params={"vector_store_ids": []} + ) + + assert logging_obj.get_chat_completion_prompt( + model="gemini-2.5-flash", + messages=messages, + non_default_params={"vector_store_ids": []}, + prompt_variables=None, + prompt_id=None, + ) == ("gemini-2.5-flash", messages, {"vector_store_ids": []}) + + assert dotprompt_manager.get_chat_completion_prompt( + model="gemini-2.5-flash", + messages=messages, + non_default_params={}, + prompt_id=None, + prompt_variables=None, + dynamic_callback_params={}, + ) == ("gemini-2.5-flash", messages, {}) + + assert logging_obj.should_run_prompt_management_hooks( + prompt_id=None, non_default_params={"vector_store_ids": ["vs_123"]} + ) + assert isinstance( + logging_obj.get_custom_logger_for_prompt_management( + model="gemini-2.5-flash", + non_default_params={"vector_store_ids": ["vs_123"]}, + prompt_id=None, + dynamic_callback_params={}, + ), + VectorStorePreCallHook, + ) + + assert isinstance( + logging_obj.get_custom_logger_for_prompt_management( + model="gemini-2.5-flash", + non_default_params={}, + prompt_id="stem", + dynamic_callback_params={}, + ), + DotpromptManager, + ) + finally: + litellm.logging_callback_manager.remove_callback_from_list_by_object(litellm.callbacks, dotprompt_manager) + litellm.logging_callback_manager.remove_callback_from_list_by_object( + litellm._async_success_callback, dotprompt_manager + ) + for hook in [cb for cb in litellm.callbacks if isinstance(cb, VectorStorePreCallHook)]: + litellm.logging_callback_manager.remove_callback_from_list_by_object(litellm.callbacks, hook)