mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-05 08:07:05 +00:00
feat: add tags in project
This commit is contained in:
parent
50bf2da05e
commit
d3d11fb06e
5 changed files with 108 additions and 10 deletions
|
|
@ -2280,6 +2280,9 @@ class LiteLLM_VerificationTokenView(LiteLLM_VerificationToken):
|
|||
organization_rpm_limit: Optional[int] = None
|
||||
organization_metadata: Optional[dict] = None
|
||||
|
||||
# Project Params
|
||||
project_metadata: Optional[dict] = None
|
||||
|
||||
# Time stamps
|
||||
last_refreshed_at: Optional[float] = None # last time joint view was pulled from db
|
||||
|
||||
|
|
@ -2581,6 +2584,7 @@ class NewProjectRequest(LiteLLM_BudgetTable):
|
|||
team_id: str
|
||||
budget_id: Optional[str] = None
|
||||
metadata: Optional[dict] = None
|
||||
tags: Optional[List[str]] = None
|
||||
models: List[str] = []
|
||||
model_rpm_limit: Optional[dict] = None
|
||||
model_tpm_limit: Optional[dict] = None
|
||||
|
|
@ -2590,7 +2594,15 @@ class NewProjectRequest(LiteLLM_BudgetTable):
|
|||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def set_model_info(cls, values):
|
||||
for field in LiteLLM_ManagementEndpoint_MetadataFields:
|
||||
if "tags" in values and values["tags"] is not None:
|
||||
if not isinstance(values["tags"], list):
|
||||
raise ValueError(
|
||||
f"tags must be a list of strings, got {type(values['tags']).__name__}"
|
||||
)
|
||||
for field in (
|
||||
LiteLLM_ManagementEndpoint_MetadataFields
|
||||
+ LiteLLM_ManagementEndpoint_MetadataFields_Premium
|
||||
):
|
||||
if values.get(field) is not None:
|
||||
if values.get("metadata") is None:
|
||||
values.update({"metadata": {}})
|
||||
|
|
@ -2607,6 +2619,7 @@ class UpdateProjectRequest(LiteLLM_BudgetTable):
|
|||
description: Optional[str] = None
|
||||
team_id: Optional[str] = None
|
||||
metadata: Optional[dict] = None
|
||||
tags: Optional[List[str]] = None
|
||||
models: Optional[List[str]] = None
|
||||
model_rpm_limit: Optional[dict] = None
|
||||
model_tpm_limit: Optional[dict] = None
|
||||
|
|
@ -2617,7 +2630,15 @@ class UpdateProjectRequest(LiteLLM_BudgetTable):
|
|||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def set_model_info(cls, values):
|
||||
for field in LiteLLM_ManagementEndpoint_MetadataFields:
|
||||
if "tags" in values and values["tags"] is not None:
|
||||
if not isinstance(values["tags"], list):
|
||||
raise ValueError(
|
||||
f"tags must be a list of strings, got {type(values['tags']).__name__}"
|
||||
)
|
||||
for field in (
|
||||
LiteLLM_ManagementEndpoint_MetadataFields
|
||||
+ LiteLLM_ManagementEndpoint_MetadataFields_Premium
|
||||
):
|
||||
if values.get(field) is not None:
|
||||
if values.get("metadata") is None:
|
||||
values.update({"metadata": {}})
|
||||
|
|
|
|||
|
|
@ -212,10 +212,12 @@ async def user_api_key_auth_websocket(websocket: WebSocket):
|
|||
api_key = websocket.headers.get("api-key")
|
||||
if not api_key:
|
||||
# Try extracting from WebSocket subprotocol (browser clients)
|
||||
for protocol in websocket.headers.get("sec-websocket-protocol", "").split(","):
|
||||
for protocol in websocket.headers.get("sec-websocket-protocol", "").split(
|
||||
","
|
||||
):
|
||||
protocol = protocol.strip()
|
||||
if protocol.startswith("openai-insecure-api-key."):
|
||||
api_key = protocol[len("openai-insecure-api-key."):]
|
||||
api_key = protocol[len("openai-insecure-api-key.") :]
|
||||
break
|
||||
if not api_key:
|
||||
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
|
||||
|
|
@ -704,6 +706,8 @@ async def _user_api_key_auth_builder( # noqa: PLR0915
|
|||
user_api_key_cache=user_api_key_cache,
|
||||
proxy_logging_obj=proxy_logging_obj,
|
||||
)
|
||||
if _jwt_project_obj is not None:
|
||||
valid_token.project_metadata = _jwt_project_obj.metadata
|
||||
|
||||
# run through common checks
|
||||
_ = await common_checks(
|
||||
|
|
@ -1294,6 +1298,8 @@ async def _user_api_key_auth_builder( # noqa: PLR0915
|
|||
user_api_key_cache=user_api_key_cache,
|
||||
proxy_logging_obj=proxy_logging_obj,
|
||||
)
|
||||
if _project_obj is not None:
|
||||
valid_token.project_metadata = _project_obj.metadata
|
||||
|
||||
global_proxy_spend = None
|
||||
if (
|
||||
|
|
@ -1743,6 +1749,8 @@ async def _run_post_custom_auth_checks(
|
|||
user_api_key_cache=user_api_key_cache,
|
||||
proxy_logging_obj=proxy_logging_obj,
|
||||
)
|
||||
if _project_obj is not None:
|
||||
valid_token.project_metadata = _project_obj.metadata
|
||||
|
||||
_ = await common_checks(
|
||||
request=request,
|
||||
|
|
|
|||
|
|
@ -248,13 +248,15 @@ def clean_headers(
|
|||
clean_headers = {}
|
||||
litellm_key_lower = (
|
||||
litellm_key_header_name.lower() if litellm_key_header_name is not None else None
|
||||
)
|
||||
)
|
||||
for header, value in headers.items():
|
||||
header_lower = header.lower()
|
||||
|
||||
|
||||
if header_lower == "authorization" and is_anthropic_oauth_key(value):
|
||||
clean_headers[header] = value
|
||||
elif forward_llm_provider_auth_headers and header_lower in _SPECIAL_HEADERS_CACHE:
|
||||
elif (
|
||||
forward_llm_provider_auth_headers and header_lower in _SPECIAL_HEADERS_CACHE
|
||||
):
|
||||
if litellm_key_lower and header_lower == litellm_key_lower:
|
||||
continue
|
||||
if header_lower == "authorization":
|
||||
|
|
@ -840,11 +842,13 @@ async def add_litellm_data_to_request( # noqa: PLR0915
|
|||
from litellm.types.proxy.litellm_pre_call_utils import SecretFields
|
||||
|
||||
_raw_headers: Dict[str, str] = _safe_get_request_headers(request)
|
||||
|
||||
|
||||
forward_llm_auth = False
|
||||
if general_settings:
|
||||
forward_llm_auth = general_settings.get("forward_llm_provider_auth_headers", False)
|
||||
|
||||
forward_llm_auth = general_settings.get(
|
||||
"forward_llm_provider_auth_headers", False
|
||||
)
|
||||
|
||||
_headers: Dict[str, str] = clean_headers(
|
||||
request.headers,
|
||||
litellm_key_header_name=(
|
||||
|
|
@ -1019,6 +1023,14 @@ async def add_litellm_data_to_request( # noqa: PLR0915
|
|||
"spend_logs_metadata"
|
||||
]
|
||||
|
||||
## PROJECT-LEVEL SPEND LOGS/TAGS
|
||||
project_metadata = user_api_key_dict.project_metadata or {}
|
||||
if "tags" in project_metadata and project_metadata["tags"] is not None:
|
||||
data[_metadata_variable_name]["tags"] = LiteLLMProxyRequestSetup._merge_tags(
|
||||
request_tags=data[_metadata_variable_name].get("tags"),
|
||||
tags_to_add=project_metadata["tags"],
|
||||
)
|
||||
|
||||
## TEAM-LEVEL METADATA
|
||||
data = (
|
||||
LiteLLMProxyRequestSetup.add_management_endpoint_metadata_to_request_metadata(
|
||||
|
|
|
|||
|
|
@ -284,6 +284,7 @@ async def new_project(
|
|||
- model_tpm_limit: *Optional[dict]* - TPM limits per model. Example: {"gpt-4": 50000, "gpt-3.5-turbo": 100000}
|
||||
- budget_duration: *Optional[str]* - Frequency of reseting project budget
|
||||
- metadata: *Optional[dict]* - Metadata for project, store information for project. Example metadata - {"use_case_id": "SNOW-12345", "responsible_ai_id": "RAI-67890"}
|
||||
- tags: *Optional[list]* - Tags for the project. Example: ["production", "api"]
|
||||
- blocked: *bool* - Flag indicating if the project is blocked or not - will stop all calls from keys with this project_id.
|
||||
- object_permission: Optional[LiteLLM_ObjectPermissionBase] - project-specific object permission. Example - {"vector_stores": ["vector_store_1", "vector_store_2"]}. IF null or {} then no object permission.
|
||||
|
||||
|
|
@ -339,6 +340,15 @@ async def new_project(
|
|||
)
|
||||
|
||||
try:
|
||||
if getattr(data, "tags", None) is not None and not premium_user:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail={
|
||||
"error": "Only premium users can add tags to projects. "
|
||||
+ CommonProxyErrors.not_premium_user.value
|
||||
},
|
||||
)
|
||||
|
||||
if not premium_user:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
|
|
@ -485,6 +495,7 @@ async def update_project(
|
|||
- model_rpm_limit: *Optional[dict]* - Updated RPM limits per model
|
||||
- model_tpm_limit: *Optional[dict]* - Updated TPM limits per model
|
||||
- budget_duration: *Optional[str]* - Updated budget duration
|
||||
- tags: *Optional[list]* - Updated list of tags for the project
|
||||
- object_permission: Optional[LiteLLM_ObjectPermissionBase] - Updated object permission
|
||||
|
||||
Example:
|
||||
|
|
@ -514,6 +525,15 @@ async def update_project(
|
|||
)
|
||||
|
||||
try:
|
||||
if getattr(data, "tags", None) is not None and not premium_user:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail={
|
||||
"error": "Only premium users can add tags to projects. "
|
||||
+ CommonProxyErrors.not_premium_user.value
|
||||
},
|
||||
)
|
||||
|
||||
if not premium_user:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
|
|
|
|||
37
tests/test_litellm/test_project_tags_pydantic.py
Normal file
37
tests/test_litellm/test_project_tags_pydantic.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import pytest
|
||||
from litellm.proxy._types import NewProjectRequest, UpdateProjectRequest
|
||||
|
||||
|
||||
def test_new_project_request_tags():
|
||||
# Test tags are correctly moved to metadata["tags"]
|
||||
req = NewProjectRequest(
|
||||
project_id="test_proj", team_id="team_1", tags=["tag1", "tag2"]
|
||||
)
|
||||
|
||||
# After validation, tags should be inside metadata
|
||||
assert req.metadata is not None
|
||||
assert "tags" in req.metadata
|
||||
assert req.metadata["tags"] == ["tag1", "tag2"]
|
||||
assert req.tags is None # Or removed dependending on pydantic version
|
||||
|
||||
|
||||
def test_update_project_request_tags():
|
||||
# Test tags are correctly moved to metadata["tags"]
|
||||
req = UpdateProjectRequest(project_id="test_proj", tags=["new_tag"])
|
||||
|
||||
assert req.metadata is not None
|
||||
assert "tags" in req.metadata
|
||||
assert req.metadata["tags"] == ["new_tag"]
|
||||
assert req.tags is None
|
||||
|
||||
|
||||
def test_new_project_request_invalid_tags_type():
|
||||
# tags must be a list — a string should raise a ValidationError
|
||||
with pytest.raises(Exception):
|
||||
NewProjectRequest(project_id="test_proj", team_id="team_1", tags="not-a-list")
|
||||
|
||||
|
||||
def test_update_project_request_invalid_tags_type():
|
||||
# tags must be a list — a string should raise a ValidationError
|
||||
with pytest.raises(Exception):
|
||||
UpdateProjectRequest(project_id="test_proj", tags="not-a-list")
|
||||
Loading…
Add table
Reference in a new issue