From 6ae22908b74779ae4bf43565a9fc02a0ff639e7e Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Fri, 21 Nov 2025 13:58:49 -0800 Subject: [PATCH] [Feat] Prompt Versioning - Allow specifying prompt version in code (#16929) * add _get_prompt_data_from_dotprompt_content * fix pre call hook for prompt template * fix: get_latest_version_prompt_id * fix get_latest_version_prompt_id * test_get_latest_version_prompt_id --- litellm/integrations/dotprompt/__init__.py | 22 +++++ ...odel_prices_and_context_window_backup.json | 47 +++++++++++ litellm/proxy/prompts/prompt_endpoints.py | 66 +++++++++++++++ litellm/proxy/utils.py | 21 ++++- .../proxy/prompts/test_prompt_endpoints.py | 81 +++++++++++++++++++ 5 files changed, 235 insertions(+), 2 deletions(-) diff --git a/litellm/integrations/dotprompt/__init__.py b/litellm/integrations/dotprompt/__init__.py index 3af7fbf6dd3..3847c8fa192 100644 --- a/litellm/integrations/dotprompt/__init__.py +++ b/litellm/integrations/dotprompt/__init__.py @@ -25,6 +25,23 @@ def set_global_prompt_directory(directory: str) -> None: litellm.global_prompt_directory = directory # type: ignore +def _get_prompt_data_from_dotprompt_content(dotprompt_content: str) -> dict: + """ + Get the prompt data from the dotprompt content. + + The UI stores prompts under `dotprompt_content` in the database. This function parses the content and returns the prompt data in the format expected by the prompt manager. + """ + from .prompt_manager import PromptManager + + # Parse the dotprompt content to extract frontmatter and content + temp_manager = PromptManager() + metadata, content = temp_manager._parse_frontmatter(dotprompt_content) + + # Convert to prompt_data format + return { + "content": content.strip(), + "metadata": metadata + } def prompt_initializer( litellm_params: "PromptLiteLLMParams", prompt_spec: "PromptSpec" @@ -41,6 +58,11 @@ def prompt_initializer( ) prompt_file = getattr(litellm_params, "prompt_file", None) + + # Handle dotprompt_content from database + dotprompt_content = getattr(litellm_params, "dotprompt_content", None) + if dotprompt_content and not prompt_data and not prompt_file: + prompt_data = _get_prompt_data_from_dotprompt_content(dotprompt_content) try: dot_prompt_manager = DotpromptManager( diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index b4b4f763860..273502ac22b 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -19977,6 +19977,53 @@ "supports_tool_choice": true, "supports_vision": true }, + "openrouter/google/gemini-3-pro-preview": { + "cache_read_input_token_cost": 2e-07, + "cache_read_input_token_cost_above_200k_tokens": 4e-07, + "cache_creation_input_token_cost_above_200k_tokens": 2.5e-07, + "input_cost_per_token": 2e-06, + "input_cost_per_token_above_200k_tokens": 4e-06, + "input_cost_per_token_batches": 1e-06, + "litellm_provider": "openrouter", + "max_audio_length_hours": 8.4, + "max_audio_per_prompt": 1, + "max_images_per_prompt": 3000, + "max_input_tokens": 1048576, + "max_output_tokens": 65535, + "max_pdf_size_mb": 30, + "max_tokens": 65535, + "max_video_length": 1, + "max_videos_per_prompt": 10, + "mode": "chat", + "output_cost_per_token": 1.2e-05, + "output_cost_per_token_above_200k_tokens": 1.8e-05, + "output_cost_per_token_batches": 6e-06, + "supported_endpoints": [ + "/v1/chat/completions", + "/v1/completions", + "/v1/batch" + ], + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_input": true, + "supports_function_calling": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "supports_video_input": true, + "supports_vision": true, + "supports_web_search": true + }, "openrouter/google/gemini-pro-1.5": { "input_cost_per_image": 0.00265, "input_cost_per_token": 2.5e-06, diff --git a/litellm/proxy/prompts/prompt_endpoints.py b/litellm/proxy/prompts/prompt_endpoints.py index 2861e8594a4..d912a416120 100644 --- a/litellm/proxy/prompts/prompt_endpoints.py +++ b/litellm/proxy/prompts/prompt_endpoints.py @@ -77,6 +77,72 @@ def get_version_number(prompt_id: str) -> int: return 1 +def construct_versioned_prompt_id(prompt_id: str, version: Optional[int] = None) -> str: + """ + Construct a versioned prompt ID from a base prompt_id and version number. + + Args: + prompt_id: Base prompt ID (e.g., "jack_success") + version: Version number (if None, returns the base prompt_id unchanged) + + Returns: + Versioned prompt ID (e.g., "jack_success.v4") + + Examples: + >>> construct_versioned_prompt_id("jack_success", 4) + "jack_success.v4" + >>> construct_versioned_prompt_id("jack_success", None) + "jack_success" + >>> construct_versioned_prompt_id("jack_success.v2", 4) + "jack_success.v4" + """ + if version is None: + return prompt_id + + # Strip any existing version suffix first + base_id = get_base_prompt_id(prompt_id) + return f"{base_id}.v{version}" + + +def get_latest_version_prompt_id(prompt_id: str, all_prompt_ids: Dict[str, Any]) -> str: + """ + Find the latest version of a prompt from available prompt IDs. + + Args: + prompt_id: Base prompt ID or versioned prompt ID (e.g., "jack_success" or "jack_success.v2") + all_prompt_ids: Dictionary of all available prompt IDs (keys are prompt IDs) + + Returns: + The prompt ID with the highest version number, or the original prompt_id if no versions exist + + Examples: + >>> all_ids = {"jack.v1": {}, "jack.v2": {}, "jack.v3": {}} + >>> get_latest_version_prompt_id("jack", all_ids) + "jack.v3" + >>> get_latest_version_prompt_id("jack.v1", all_ids) + "jack.v3" + >>> all_ids = {"simple": {}} + >>> get_latest_version_prompt_id("simple", all_ids) + "simple" + """ + base_id = get_base_prompt_id(prompt_id=prompt_id) + + # Find all versions of this prompt + matching_versions = [] + for stored_prompt_id in all_prompt_ids.keys(): + if get_base_prompt_id(prompt_id=stored_prompt_id) == base_id: + version_num = get_version_number(prompt_id=stored_prompt_id) + matching_versions.append((version_num, stored_prompt_id)) + + # Use the highest version number + if matching_versions: + matching_versions.sort(reverse=True) + return matching_versions[0][1] + else: + # No versioned prompts found, use the base ID as-is + return prompt_id + + def get_latest_prompt_versions(prompts: List[PromptSpec]) -> List[PromptSpec]: """ Filter a list of prompts to return only the latest version of each unique prompt. diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index 9747329cd4f..5ec8aecfef0 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -894,6 +894,7 @@ class ProxyLogging: Optional["LiteLLMLoggingObj"], data.get("litellm_logging_obj", None) ) prompt_id = data.get("prompt_id", None) + prompt_version = data.get("prompt_version", None) ## PROMPT TEMPLATE CHECK ## if ( @@ -901,12 +902,28 @@ class ProxyLogging: and prompt_id is not None and (call_type == "completion" or call_type == "acompletion") ): + from litellm.proxy.prompts.prompt_endpoints import ( + construct_versioned_prompt_id, + get_latest_version_prompt_id, + ) from litellm.proxy.prompts.prompt_registry import IN_MEMORY_PROMPT_REGISTRY + # If no version is specified, find the latest version + if prompt_version is None: + lookup_prompt_id = get_latest_version_prompt_id( + prompt_id=prompt_id, + all_prompt_ids=IN_MEMORY_PROMPT_REGISTRY.IN_MEMORY_PROMPTS, + ) + else: + # Construct versioned prompt_id if prompt_version is provided + lookup_prompt_id = construct_versioned_prompt_id( + prompt_id=prompt_id, version=prompt_version + ) + custom_logger = IN_MEMORY_PROMPT_REGISTRY.get_prompt_callback_by_id( - prompt_id + lookup_prompt_id ) - prompt_spec = IN_MEMORY_PROMPT_REGISTRY.get_prompt_by_id(prompt_id) + prompt_spec = IN_MEMORY_PROMPT_REGISTRY.get_prompt_by_id(lookup_prompt_id) litellm_prompt_id: Optional[str] = None if prompt_spec is not None: litellm_prompt_id = prompt_spec.litellm_params.prompt_id diff --git a/tests/test_litellm/proxy/prompts/test_prompt_endpoints.py b/tests/test_litellm/proxy/prompts/test_prompt_endpoints.py index bf296f2a03d..a492606a3ac 100644 --- a/tests/test_litellm/proxy/prompts/test_prompt_endpoints.py +++ b/tests/test_litellm/proxy/prompts/test_prompt_endpoints.py @@ -104,6 +104,87 @@ class TestPromptVersioning: assert get_base_prompt_id(prompt_id="jack") == "jack" assert get_base_prompt_id(prompt_id="my_prompt.v10") == "my_prompt" + def test_get_latest_version_prompt_id(self): + """ + Test that get_latest_version_prompt_id returns the highest version + """ + from litellm.proxy.prompts.prompt_endpoints import get_latest_version_prompt_id + + # Mock prompt IDs dictionary + all_prompt_ids = { + "jack.v1": {}, + "jack.v2": {}, + "jack.v3": {}, + "jane.v1": {}, + "simple_prompt": {}, + } + + # Test with base prompt ID - should return latest version + assert get_latest_version_prompt_id( + prompt_id="jack", + all_prompt_ids=all_prompt_ids + ) == "jack.v3" + + # Test with versioned prompt ID - should still return latest version + assert get_latest_version_prompt_id( + prompt_id="jack.v1", + all_prompt_ids=all_prompt_ids + ) == "jack.v3" + + # Test with single version + assert get_latest_version_prompt_id( + prompt_id="jane", + all_prompt_ids=all_prompt_ids + ) == "jane.v1" + + # Test with non-versioned prompt + assert get_latest_version_prompt_id( + prompt_id="simple_prompt", + all_prompt_ids=all_prompt_ids + ) == "simple_prompt" + + # Test with non-existent prompt + assert get_latest_version_prompt_id( + prompt_id="nonexistent", + all_prompt_ids=all_prompt_ids + ) == "nonexistent" + + def test_construct_versioned_prompt_id(self): + """ + Test that construct_versioned_prompt_id correctly builds versioned IDs + """ + from litellm.proxy.prompts.prompt_endpoints import construct_versioned_prompt_id + + # Test with base prompt ID and version + assert construct_versioned_prompt_id( + prompt_id="jack_success", + version=4 + ) == "jack_success.v4" + + # Test with None version - should return base ID unchanged + assert construct_versioned_prompt_id( + prompt_id="jack_success", + version=None + ) == "jack_success" + + # Test with existing versioned ID - should replace version + assert construct_versioned_prompt_id( + prompt_id="jack_success.v2", + version=4 + ) == "jack_success.v4" + + # Test with hyphenated prompt ID + assert construct_versioned_prompt_id( + prompt_id="my-prompt", + version=1 + ) == "my-prompt.v1" + + # Test with double-digit version + assert construct_versioned_prompt_id( + prompt_id="test_prompt", + version=10 + ) == "test_prompt.v10" + class TestPromptVersionsEndpoint: """