[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
This commit is contained in:
Ishaan Jaff 2025-11-21 13:58:49 -08:00 • committed by GitHub
parent 4b25398afe
commit 6ae22908b7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 235 additions and 2 deletions

View file

@ -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(

View file

@ -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,

View file

@ -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.

View file

@ -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

View file

@ -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:
"""