Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 69 additions & 1 deletion litellm/proxy/auth/handle_jwt.py
Original file line number Diff line number Diff line change
Expand Up @@ -1451,6 +1451,61 @@ async def _attach_team_from_header_for_admin(
admin_result["team_id"] = header_team_id
admin_result["team_object"] = team_object

@staticmethod
async def _resolve_single_team_fallback(
user_object: Optional[LiteLLM_UserTable],
user_id: Optional[str],
prisma_client: Optional[PrismaClient],
user_api_key_cache: DualCache,
parent_otel_span: Optional[Span],
proxy_logging_obj: ProxyLogging,
team_id_upsert: Optional[bool],
) -> tuple:
"""
If JWT did not resolve team_id, but the user belongs to exactly one team
in LiteLLM, load that team (and membership when user_id is set) so that
spend / metadata can be attributed correctly.

Returns (team_id, team_object, team_membership_object).
Any DB error is debug-logged and the tuple is (None, None, None) — no
exception ever propagates from this helper.
"""
if user_object is None or not user_object.teams or len(user_object.teams) != 1:
return None, None, None

_tid = user_object.teams[0]
try:
team_row = await get_team_object(
team_id=_tid,
prisma_client=prisma_client,
user_api_key_cache=user_api_key_cache,
parent_otel_span=parent_otel_span,
proxy_logging_obj=proxy_logging_obj,
team_id_upsert=team_id_upsert,
)
if team_row is None:
return None, None, None

if not user_id:
return _tid, team_row, None

team_membership = await get_team_membership(
user_id=user_id,
team_id=_tid,
prisma_client=prisma_client,
user_api_key_cache=user_api_key_cache,
parent_otel_span=parent_otel_span,
proxy_logging_obj=proxy_logging_obj,
)
return _tid, team_row, team_membership
except Exception:
verbose_proxy_logger.debug(
"JWT single-team fallback error, skipping. team_id=%s",
_tid,
exc_info=True,
)
return None, None, None

@staticmethod
async def auth_builder(
api_key: str,
Expand Down Expand Up @@ -1515,7 +1570,6 @@ async def auth_builder(
object_id = jwt_handler.get_object_id(token=jwt_valid_token, default_value=None)

# Get basic user info
scopes = jwt_handler.get_scopes(token=jwt_valid_token)
user_id, user_email, valid_user_email = await JWTAuthManager.get_user_info(
jwt_handler, jwt_valid_token
)
Expand Down Expand Up @@ -1637,6 +1691,20 @@ async def auth_builder(
user_api_key_cache=user_api_key_cache,
)

# If JWT did not resolve team_id, attempt single-team DB fallback.
if team_id is None:
team_id, team_object, team_membership_object = (
await JWTAuthManager._resolve_single_team_fallback(
user_object=user_object,
user_id=user_id,
prisma_client=prisma_client,
user_api_key_cache=user_api_key_cache,
parent_otel_span=parent_otel_span,
proxy_logging_obj=proxy_logging_obj,
team_id_upsert=jwt_handler.litellm_jwtauth.team_id_upsert,
)
)

## MAP USER TO TEAMS
await JWTAuthManager.map_user_to_teams(
user_object=user_object,
Expand Down
Loading
Loading