Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/auth/team_grants.py: 61%
56 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
1"""Project a team row (plus the caller's membership in it) onto the ``team_*`` fields of ``UserAPIKeyAuth``.
3The virtual-key path gets these fields for free from the combined-view SQL join. Every other auth path
4starts from a ``LiteLLM_TeamTable`` object instead and has to copy them over by hand, which is how JWT
5callers kept losing grants (aliases, permissions, limits) one field at a time. Build the badge through
6``team_grants`` and the two paths cannot drift.
7"""
9from collections.abc import Mapping, Sequence
10from types import MappingProxyType
11from typing import Annotated, Final
13from pydantic import BaseModel, BeforeValidator, ConfigDict, TypeAdapter, ValidationError
14from pydantic.main import IncEx
15from typing_extensions import ReadOnly, TypedDict
17from litellm.proxy._types import (
18 LiteLLM_ObjectPermissionTable,
19 LiteLLM_TeamMembership,
20 LiteLLM_TeamTable,
21 Member,
22)
24_MODEL_ALIASES_ADAPTER: Final = TypeAdapter(dict[str, str])
25_JSON_COLUMNS: Final[Mapping[str, IncEx | bool]] = MappingProxyType(
26 {"metadata": True, "litellm_model_table": MappingProxyType({"model_aliases": True})}
27)
30def _decode_model_aliases(value: object) -> object:
31 """``LiteLLM_ModelTable.model_aliases`` is typed ``str | dict``; writers hand Prisma ``json.dumps(...)``, so take both."""
32 if not isinstance(value, str):
33 return value
34 try:
35 return _MODEL_ALIASES_ADAPTER.validate_json(value)
36 except ValidationError:
37 return None
40class TeamModelAliasTable(BaseModel):
41 model_config = ConfigDict(protected_namespaces=())
43 model_aliases: Annotated[Mapping[str, str] | None, BeforeValidator(_decode_model_aliases)] = None
46class _TeamJsonColumns(BaseModel):
47 """The two loosely typed columns on ``LiteLLM_TeamTable``, re-read with the shape the badge needs."""
49 metadata: Mapping[str, object] | None = None
50 litellm_model_table: TeamModelAliasTable | None = None
53class TeamGrants(TypedDict, total=False):
54 """Keyword arguments for ``UserAPIKeyAuth``. Empty when the caller has no team, so the model's own defaults apply."""
56 team_alias: ReadOnly[str | None]
57 team_tpm_limit: ReadOnly[int | None]
58 team_rpm_limit: ReadOnly[int | None]
59 team_tpd_limit: ReadOnly[int | None]
60 team_max_budget: ReadOnly[float | None]
61 team_soft_budget: ReadOnly[float | None]
62 team_model_max_budget: ReadOnly[dict[str, object] | None]
63 team_spend: ReadOnly[float | None]
64 team_models: ReadOnly[Sequence[str]]
65 team_blocked: ReadOnly[bool]
66 team_metadata: ReadOnly[Mapping[str, object] | None]
67 team_model_aliases: ReadOnly[Mapping[str, str] | None]
68 team_object_permission_id: ReadOnly[str | None]
69 team_object_permission: ReadOnly[LiteLLM_ObjectPermissionTable | None]
70 team_member: ReadOnly[Member | None]
71 team_member_spend: ReadOnly[float | None]
72 team_member_tpm_limit: ReadOnly[int | None]
73 team_member_rpm_limit: ReadOnly[int | None]
76def _json_columns(team_object: LiteLLM_TeamTable) -> _TeamJsonColumns:
77 try:
78 return _TeamJsonColumns.model_validate(team_object.model_dump(include=_JSON_COLUMNS))
79 except ValidationError:
80 return _TeamJsonColumns()
83def team_model_aliases(team_object: LiteLLM_TeamTable | None) -> Mapping[str, str] | None:
84 if team_object is None:
85 return None
86 alias_table: Final = _json_columns(team_object).litellm_model_table
87 return alias_table.model_aliases if alias_table is not None else None
90def team_grants(
91 team_object: LiteLLM_TeamTable | None,
92 team_membership: LiteLLM_TeamMembership | None,
93 user_id: str | None,
94) -> TeamGrants:
95 if team_object is None:
96 return TeamGrants()
97 json_columns: Final = _json_columns(team_object)
98 return TeamGrants(
99 team_alias=team_object.team_alias,
100 team_tpm_limit=team_object.tpm_limit,
101 team_rpm_limit=team_object.rpm_limit,
102 team_tpd_limit=team_object.tpd_limit,
103 team_max_budget=team_object.max_budget,
104 team_soft_budget=team_object.soft_budget,
105 team_model_max_budget=team_object.model_max_budget,
106 team_spend=team_object.spend,
107 team_models=tuple(team_object.models),
108 team_blocked=team_object.blocked,
109 team_metadata=json_columns.metadata,
110 team_model_aliases=(
111 json_columns.litellm_model_table.model_aliases if json_columns.litellm_model_table is not None else None
112 ),
113 team_object_permission_id=team_object.object_permission_id,
114 team_object_permission=team_object.object_permission,
115 team_member=next(
116 (m for m in team_object.members_with_roles if user_id is not None and m.user_id == user_id),
117 None,
118 ),
119 team_member_spend=team_membership.spend if team_membership is not None else None,
120 team_member_tpm_limit=(
121 team_membership.safe_get_team_member_tpm_limit() if team_membership is not None else None
122 ),
123 team_member_rpm_limit=(
124 team_membership.safe_get_team_member_rpm_limit() if team_membership is not None else None
125 ),
126 )