-
-
Notifications
You must be signed in to change notification settings - Fork 6k
feat: team scoped model overrides #22308
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
a1b79ed
e2276c2
f2aeb6e
350e2d8
7708d08
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -65,6 +65,7 @@ | |
|
|
||
| from .auth_checks_organization import organization_role_based_access_check | ||
| from .auth_utils import get_model_from_request | ||
| from litellm.proxy.management_endpoints.common_utils import _is_team_model_overrides_enabled | ||
|
|
||
| if TYPE_CHECKING: | ||
| from opentelemetry.trace import Span as _Span | ||
|
|
@@ -273,6 +274,7 @@ async def common_checks( | |
| team_object=team_object, | ||
| llm_router=llm_router, | ||
| team_model_aliases=valid_token.team_model_aliases if valid_token else None, | ||
| valid_token=valid_token, | ||
| ): | ||
| raise ProxyException( | ||
| message=f"Team not allowed to access model. Team={team_object.team_id}, Model={_model}. Allowed team models = {team_object.models}", | ||
|
|
@@ -2557,23 +2559,51 @@ def can_org_access_model( | |
| ) | ||
|
|
||
|
|
||
| def compute_effective_team_models( | ||
| team_default_models: List[str], | ||
| team_member_models: List[str], | ||
| ) -> List[str]: | ||
| """Union of team defaults and per-user overrides, deduplicated.""" | ||
| return list(set(team_default_models) | set(team_member_models)) | ||
|
|
||
|
|
||
| async def can_team_access_model( | ||
| model: Union[str, List[str]], | ||
| team_object: Optional[LiteLLM_TeamTable], | ||
| llm_router: Optional[Router], | ||
| team_model_aliases: Optional[Dict[str, str]] = None, | ||
| valid_token: Optional[UserAPIKeyAuth] = None, | ||
| ) -> Literal[True]: | ||
| """ | ||
| Returns True if the team can access a specific model. | ||
|
|
||
| 1. First checks native team-level model permissions (current implementation) | ||
| 2. If not allowed natively, falls back to access_group_ids on the team | ||
| """ | ||
| models_to_check: List[str] = team_object.models if team_object else [] | ||
| if _is_team_model_overrides_enabled() and valid_token: | ||
| # Compute effective models: team defaults + per-user overrides | ||
| effective_models = compute_effective_team_models( | ||
| team_default_models=valid_token.team_default_models, | ||
| team_member_models=valid_token.team_member_models, | ||
| ) | ||
|
|
||
| # If effective_models is empty, and feature is enabled, deny access | ||
| if len(effective_models) == 0: | ||
| raise ProxyException( | ||
| message=f"Team not allowed to access model. No models available for user in this team. Model={model}.", | ||
| type=ProxyErrorTypes.team_model_access_denied, | ||
| param="model", | ||
| code=status.HTTP_403_FORBIDDEN, | ||
| ) | ||
|
|
||
| models_to_check = effective_models | ||
|
Comment on lines
+2583
to
+2600
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Override mode silently ignores When Consider falling back to |
||
|
|
||
| try: | ||
| return _can_object_call_model( | ||
| model=model, | ||
| llm_router=llm_router, | ||
| models=team_object.models if team_object else [], | ||
| models=models_to_check, | ||
| team_model_aliases=team_model_aliases, | ||
| team_id=team_object.team_id if team_object else None, | ||
| object_type="team", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -890,6 +890,65 @@ async def _check_team_key_limits( | |
| ) | ||
|
|
||
|
|
||
| async def _validate_key_models_against_effective_team_models( | ||
| team_id: str, | ||
| user_id: Optional[str], | ||
| data: Union[GenerateKeyRequest, UpdateKeyRequest], | ||
| team_table: LiteLLM_TeamTableCachedObj, | ||
| prisma_client: PrismaClient, | ||
| ) -> None: | ||
| """ | ||
| Validate that the requested models for a key are a subset of the effective team models. | ||
| Effective models = team.default_models ∪ membership.models | ||
| """ | ||
| from litellm.proxy.management_endpoints.common_utils import ( | ||
| _is_team_model_overrides_enabled, | ||
| ) | ||
|
|
||
| if not _is_team_model_overrides_enabled(): | ||
| return | ||
|
|
||
| # 1. Fetch team membership if user_id is provided | ||
| member_models: List[str] = [] | ||
| if user_id: | ||
| membership = await prisma_client.db.litellm_teammembership.find_unique( | ||
| where={"user_id_team_id": {"user_id": user_id, "team_id": team_id}} | ||
|
Comment on lines
+914
to
+915
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Direct DB query bypasses caching layer This |
||
| ) | ||
| if membership: | ||
| member_models = membership.models or [] | ||
|
|
||
| # 2. Compute effective models | ||
| from litellm.proxy.auth.auth_checks import compute_effective_team_models | ||
|
|
||
| effective_models = compute_effective_team_models( | ||
| team_default_models=team_table.default_models or [], | ||
| team_member_models=member_models, | ||
| ) | ||
|
|
||
| # 3. Step 6b: If effective models are empty, deny access (empty list != all access) | ||
| if not effective_models: | ||
| raise HTTPException( | ||
| status_code=403, | ||
| detail={ | ||
| "error": f"No models available for User={user_id} in Team={team_id}. Admins must set 'default_models' on the team or per-user 'models' overrides." | ||
| }, | ||
| ) | ||
|
|
||
| # 4. Step 6b: If data.models is empty, default to effective models | ||
| if not data.models: | ||
| data.models = effective_models | ||
|
Comment on lines
+938
to
+939
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Empty When Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
||
| else: | ||
| # Verify requested models are a subset of effective models | ||
| for m in data.models: | ||
| if m not in effective_models: | ||
| raise HTTPException( | ||
| status_code=403, | ||
| detail={ | ||
| "error": f"Model '{m}' is not available for this user in Team={team_id}. Available models = {effective_models}" | ||
| }, | ||
| ) | ||
Harshit28j marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
|
|
||
| async def _check_project_key_limits( | ||
| project_id: str, | ||
| data: Union[GenerateKeyRequest, UpdateKeyRequest], | ||
|
|
@@ -1201,6 +1260,13 @@ async def generate_key_fn( | |
| data=data, | ||
| prisma_client=prisma_client, | ||
| ) | ||
| await _validate_key_models_against_effective_team_models( | ||
| team_id=data.team_id, | ||
| user_id=data.user_id, | ||
| data=data, | ||
| team_table=team_table, | ||
| prisma_client=prisma_client, | ||
| ) | ||
|
|
||
| # Validate key against project limits if project_id is set | ||
| if data.project_id is not None: | ||
|
|
@@ -1354,6 +1420,13 @@ async def generate_service_account_key_fn( | |
| data=data, | ||
| prisma_client=prisma_client, | ||
| ) | ||
| await _validate_key_models_against_effective_team_models( | ||
| team_id=data.team_id, | ||
| user_id=data.user_id, | ||
| data=data, | ||
| team_table=team_table, | ||
| prisma_client=prisma_client, | ||
| ) | ||
|
|
||
| key_generation_check( | ||
| team_table=team_table, | ||
|
|
@@ -1887,6 +1960,25 @@ async def update_key_fn( | |
| prisma_client=prisma_client, | ||
| ) | ||
|
|
||
| # Step 6d: Key update validation against effective models | ||
| if data.models is not None and (data.team_id or existing_key_row.team_id): | ||
| team_id_to_check = data.team_id or existing_key_row.team_id | ||
| if team_obj is None or team_obj.team_id != team_id_to_check: | ||
| team_obj = await get_team_object( | ||
| team_id=team_id_to_check, | ||
| prisma_client=prisma_client, | ||
| user_api_key_cache=user_api_key_cache, | ||
| check_db_only=True, | ||
| ) | ||
| if team_obj is not None: | ||
| await _validate_key_models_against_effective_team_models( | ||
| team_id=team_id_to_check, | ||
| user_id=data.user_id or existing_key_row.user_id, | ||
| data=data, | ||
| team_table=team_obj, | ||
| prisma_client=prisma_client, | ||
| ) | ||
|
|
||
| # Validate key against project limits if project_id is being set | ||
| _project_id_to_check = getattr(data, "project_id", None) or getattr( | ||
| existing_key_row, "project_id", None | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Top-level import in auth module
Per CLAUDE.md, top-level imports are preferred. However, this creates a direct import from
management_endpointsinto theauthmodule. Ifcommon_utilsever imports fromauth_checks(it currently doesn't, butkey_management_endpoints.pyalready does a deferredfrom litellm.proxy.auth.auth_checks import compute_effective_team_modelsat line 921), this could lead to a circular import risk. Consider whether this should remain top-level or use a deferred import like the callers inkey_management_endpoints.pydo.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!