fix: tags on projects

This commit is contained in:
Harshit28j 2026-02-26 22:17:30 +05:30
parent 4419c7a5f9
commit 2c00311599
4 changed files with 51 additions and 5 deletions

View file

@ -2257,6 +2257,7 @@ class LiteLLM_VerificationTokenView(LiteLLM_VerificationToken):
team_member: Optional[Member] = None
team_metadata: Optional[Dict] = None
team_object_permission_id: Optional[str] = None
project_metadata: Optional[Dict] = None
# Team Member Specific Params
team_member_spend: Optional[float] = None
@ -2581,11 +2582,15 @@ class NewProjectRequest(LiteLLM_BudgetTable):
model_tpm_limit: Optional[dict] = None
blocked: bool = False
object_permission: Optional[LiteLLM_ObjectPermissionBase] = None
tags: Optional[List[str]] = None
@model_validator(mode="before")
@classmethod
def set_model_info(cls, values):
for field in LiteLLM_ManagementEndpoint_MetadataFields:
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": {}})
@ -2608,11 +2613,15 @@ class UpdateProjectRequest(LiteLLM_BudgetTable):
blocked: Optional[bool] = None
budget_id: Optional[str] = None
object_permission: Optional[LiteLLM_ObjectPermissionBase] = None
tags: Optional[List[str]] = None
@model_validator(mode="before")
@classmethod
def set_model_info(cls, values):
for field in LiteLLM_ManagementEndpoint_MetadataFields:
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": {}})

View file

@ -692,6 +692,10 @@ async def _user_api_key_auth_builder( # noqa: PLR0915
project_object=_jwt_project_obj,
)
# Add project metadata for tag-based routing when key belongs to a project
if _jwt_project_obj is not None:
valid_token.project_metadata = _jwt_project_obj.metadata
# return UserAPIKeyAuth object
return cast(UserAPIKeyAuth, valid_token)
@ -1311,6 +1315,10 @@ async def _user_api_key_auth_builder( # noqa: PLR0915
valid_token_dict = valid_token.model_dump(exclude_none=True)
valid_token_dict.pop("token", None)
# Add project metadata when key belongs to a project (for tag-based routing)
if _project_obj is not None:
valid_token_dict["project_metadata"] = _project_obj.metadata
if _end_user_object is not None:
valid_token_dict.update(end_user_params)
valid_token_dict[

View file

@ -990,6 +990,15 @@ async def add_litellm_data_to_request( # noqa: PLR0915
data=data,
_metadata_variable_name=_metadata_variable_name,
)
## PROJECT-LEVEL TAGS (project sits between key and team in hierarchy)
project_metadata = getattr(
user_api_key_dict, "project_metadata", None
) 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 SPEND LOGS/TAGS
team_metadata = user_api_key_dict.team_metadata or {}
if "tags" in team_metadata and team_metadata["tags"] is not None:

View file

@ -17,7 +17,7 @@ from fastapi import APIRouter, Depends, HTTPException, Request
from litellm._logging import verbose_proxy_logger
from litellm._uuid import uuid
from litellm.proxy._types import *
from litellm.proxy._types import * # noqa: F403
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.proxy.management_endpoints.common_utils import _set_object_metadata_field
from litellm.proxy.management_helpers.utils import (
@ -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[str]]* - Tags for tracking spend and/or tag-based routing. Keys belonging to this project inherit these tags.
- 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.
@ -421,6 +422,13 @@ async def new_project(
updated_by=user_api_key_dict.user_id or litellm_proxy_admin_name,
)
for field in LiteLLM_ManagementEndpoint_MetadataFields_Premium:
if getattr(data, field, None) is not None:
_set_object_metadata_field(
object_data=project_row,
field_name=field,
value=getattr(data, field),
)
for field in LiteLLM_ManagementEndpoint_MetadataFields:
if getattr(data, field, None) is not None:
_set_object_metadata_field(
@ -478,6 +486,7 @@ async def update_project(
- team_id: *Optional[str]* - Updated team_id for the project
- metadata: *Optional[dict]* - Updated metadata for project
- models: *Optional[list]* - Updated list of models for the project
- tags: *Optional[List[str]]* - Tags for tracking spend and/or tag-based routing
- blocked: *Optional[bool]* - Updated blocked status
- max_budget: *Optional[float]* - Updated max budget
- tpm_limit: *Optional[int]* - Updated tpm limit
@ -626,13 +635,24 @@ async def update_project(
"object_permission_id"
] = created_permission.object_permission_id
# Handle metadata fields
for field in LiteLLM_ManagementEndpoint_MetadataFields:
# Handle metadata fields (Premium + standard)
for field in (
LiteLLM_ManagementEndpoint_MetadataFields_Premium
+ LiteLLM_ManagementEndpoint_MetadataFields
):
if field in update_data:
if update_data.get("metadata") is None:
update_data["metadata"] = {}
update_data["metadata"][field] = update_data.pop(field)
# Merge metadata with existing (avoid wiping metadata on partial updates like tags)
if "metadata" in update_data:
existing_metadata = (existing_project.metadata or {}) if isinstance(
existing_project.metadata, dict
) else {}
new_metadata = update_data.get("metadata") or {}
update_data["metadata"] = {**existing_metadata, **new_metadata}
# Remove budget fields (following organization_endpoints.py pattern)
update_data = _remove_budget_fields_from_project_data(update_data)