Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/management_helpers/auto_router_permissions.py: 40%

161 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-10 12:01 +0000

1from collections.abc import Mapping 

2from dataclasses import dataclass 

3from datetime import datetime 

4from types import MappingProxyType 

5from typing import TYPE_CHECKING, Final, Literal 

6 

7from fastapi import HTTPException 

8from pydantic import BaseModel, ConfigDict, Field, ValidationError 

9from typing_extensions import ReadOnly, TypedDict 

10 

11from litellm.models.organization import LiteLLM_OrganizationTable 

12from litellm.models.project import LiteLLM_ProjectTable 

13from litellm.proxy._types import ( 

14 UI_TEAM_ID, 

15 CommonProxyErrors, 

16 KeyManagementRoutes, 

17 LiteLLM_TeamMembership, 

18 LiteLLM_TeamTable, 

19 LitellmUserRoles, 

20 UserAPIKeyAuth, 

21) 

22from litellm.proxy.auth.auth_checks import ( 

23 _check_team_member_model_access, # pyright: ignore[reportPrivateUsage] # shared membership authorization owner 

24 can_key_call_model, 

25 can_org_access_model, 

26 can_project_access_model, 

27 can_team_access_model, 

28) 

29from litellm.proxy.auth.team_grants import team_model_aliases 

30from litellm.proxy.common_utils.encrypt_decrypt_utils import decrypt_value_helper 

31from litellm.repositories.organization_repository import OrganizationRepository 

32from litellm.repositories.prisma_protocols import DatabaseClient 

33from litellm.repositories.project_repository import ProjectRepository 

34from litellm.repositories.table_repositories import TeamMembershipRepository 

35from litellm.router import Router 

36from litellm.router_utils.auto_router_model_naming import classify_strategy_router_model, strategy_router_dependencies 

37from litellm.types.management_endpoints.auto_router_endpoints import RequestComplexityRouterConfig 

38from litellm.types.router import Deployment, updateDeployment 

39 

40if TYPE_CHECKING: 40 ↛ 41line 40 didn't jump to line 41 because the condition on line 40 was never true

41 from prisma import types as prisma_types 

42 

43 

44class _MemberRouterThinking(BaseModel): 

45 model_config = ConfigDict(extra="forbid", frozen=True) 

46 

47 type: Literal["enabled", "disabled", "adaptive"] 

48 budget_tokens: int | None = Field(default=None, gt=0, le=1_000_000) 

49 

50 

51class _MemberRouterGenerationParams(BaseModel): 

52 model_config = ConfigDict(extra="forbid", frozen=True) 

53 

54 reasoning_effort: str | None = None 

55 thinking: _MemberRouterThinking | None = None 

56 verbosity: Literal["low", "medium", "high"] | None = None 

57 max_tokens: int | None = Field(default=None, gt=0, le=1_000_000) 

58 max_completion_tokens: int | None = Field(default=None, gt=0, le=1_000_000) 

59 max_output_tokens: int | None = Field(default=None, gt=0, le=1_000_000) 

60 temperature: float | None = Field(default=None, ge=0, le=2, allow_inf_nan=False) 

61 top_p: float | None = Field(default=None, ge=0, le=1, allow_inf_nan=False) 

62 frequency_penalty: float | None = Field(default=None, ge=-2, le=2, allow_inf_nan=False) 

63 presence_penalty: float | None = Field(default=None, ge=-2, le=2, allow_inf_nan=False) 

64 seed: int | None = None 

65 stop: str | tuple[str, ...] | None = None 

66 

67 

68class _MemberJevClassifierConfig(BaseModel): 

69 """The Jev classifier settings a team member may set. Credentials stay the proxy's own: a member-chosen 

70 api_base would receive the proxy's TYPESAFE_API_KEY, and a member-chosen api_key would be sent from the proxy.""" 

71 

72 model_config = ConfigDict(extra="forbid") 

73 

74 model: str 

75 api_key: None = None 

76 api_base: None = None 

77 timeout_ms: int 

78 instructions: str | None = None 

79 circuit_breaker_enabled: bool 

80 circuit_breaker_cooldown_seconds: float 

81 

82 

83class _MemberComplexityRouterConfig(RequestComplexityRouterConfig): 

84 model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True) 

85 

86 

87class _RouterConfigSource(BaseModel): 

88 model: str | None = None 

89 complexity_router_config: Mapping[str, object] | None = None 

90 

91 

92class _MembershipKey(TypedDict): 

93 user_id: ReadOnly[str] 

94 team_id: ReadOnly[str] 

95 

96 

97class _MembershipWhere(TypedDict): 

98 user_id_team_id: ReadOnly[_MembershipKey] 

99 

100 

101@dataclass(frozen=True, slots=True) 

102class MemberAutoRouterDependencyObjects: 

103 membership: LiteLLM_TeamMembership | None 

104 organization: LiteLLM_OrganizationTable | None 

105 project: LiteLLM_ProjectTable | None 

106 

107 

108def authorize_member_auto_router_team( 

109 *, user_api_key_dict: UserAPIKeyAuth, team: LiteLLM_TeamTable, premium_user: bool 

110) -> None: 

111 if not premium_user: 

112 raise HTTPException(status_code=403, detail=CommonProxyErrors.not_premium_user.value) 

113 if ( 

114 user_api_key_dict.user_role 

115 not in (LitellmUserRoles.INTERNAL_USER, LitellmUserRoles.TEAM, LitellmUserRoles.ORG_ADMIN) 

116 or not user_api_key_dict.user_id 

117 or not any(member.user_id == user_api_key_dict.user_id for member in team.members_with_roles) 

118 or user_api_key_dict.team_id not in (None, UI_TEAM_ID, team.team_id) 

119 or team.blocked 

120 or KeyManagementRoutes.AUTO_ROUTER_MANAGE.value not in (team.team_member_permissions or ()) 

121 ): 

122 raise HTTPException(status_code=403, detail="This team does not allow you to manage your own auto routers.") 

123 

124 

125def validate_member_auto_router_config(config: Mapping[str, object]) -> RequestComplexityRouterConfig: 

126 try: 

127 validated: Final = _MemberComplexityRouterConfig.model_validate(config) 

128 for entries in validated.tier_model_configs.values(): 

129 for entry in entries: 

130 _MemberRouterGenerationParams.model_validate(entry.litellm_params) 

131 if validated.jev_classifier_config is not None: 

132 _MemberJevClassifierConfig.model_validate(validated.jev_classifier_config.model_dump()) 

133 return validated 

134 except ValidationError as exc: 

135 location: Final = ".".join(str(part) for part in exc.errors()[0]["loc"]) 

136 raise HTTPException(status_code=400, detail=f"Invalid member auto-router configuration at {location}.") from exc 

137 

138 

139async def authorize_member_auto_router_dependencies( 

140 *, 

141 config: RequestComplexityRouterConfig, 

142 default_model: str | None, 

143 user_api_key_dict: UserAPIKeyAuth, 

144 team: LiteLLM_TeamTable, 

145 prisma_client: DatabaseClient | None, 

146 llm_router: Router, 

147 dependency_objects: MemberAutoRouterDependencyObjects | None = None, 

148) -> None: 

149 from litellm.proxy.proxy_server import proxy_logging_obj, user_api_key_cache 

150 

151 if team.blocked: 

152 raise HTTPException(status_code=403, detail="This auto router's team is blocked.") 

153 aliases: Final = team_model_aliases(team) 

154 alias_dict: Final = ( 

155 dict(aliases) if aliases is not None else None # mutable-ok: auth model and helpers require dict 

156 ) 

157 scoped_actor: Final = user_api_key_dict.model_copy( 

158 update=MappingProxyType({"team_id": team.team_id, "team_models": team.models, "team_model_aliases": alias_dict}) 

159 ) 

160 objects: Final = ( 

161 dependency_objects 

162 if dependency_objects is not None 

163 else await _load_member_auto_router_dependency_objects( 

164 user_api_key_dict=scoped_actor, team=team, prisma_client=prisma_client 

165 ) 

166 ) 

167 if team.organization_id and objects.organization is None: 

168 raise HTTPException(status_code=403, detail="The auto router's organization is unavailable.") 

169 if scoped_actor.project_id and ( 

170 objects.project is None or objects.project.team_id != team.team_id or objects.project.blocked 

171 ): 

172 raise HTTPException(status_code=403, detail="The auto router's project is unavailable.") 

173 dependencies: Final = strategy_router_dependencies( 

174 MappingProxyType( 

175 { 

176 "model": "auto_router/complexity_router", 

177 "complexity_router_config": config.model_dump(exclude_none=True), 

178 "complexity_router_default_model": default_model, 

179 } 

180 ) 

181 ) 

182 for dependency, model, deployments in ( 

183 ( 

184 dependency, 

185 dependency.model_name, 

186 llm_router.get_model_list(model_name=dependency.model_name, team_id=team.team_id), 

187 ) 

188 for dependency in dependencies 

189 ): 

190 if dependency.role != "evaluation" and ( 

191 not deployments 

192 or any( 

193 classify_strategy_router_model( 

194 _RouterConfigSource.model_validate(deployment["litellm_params"]).model or "" 

195 ) 

196 is not None 

197 for deployment in deployments 

198 ) 

199 ): 

200 raise HTTPException(status_code=400, detail=f"Auto-router target {model!r} must be a configured model.") 

201 await can_team_access_model( 

202 model=model, 

203 team_object=team, 

204 llm_router=llm_router, 

205 team_model_aliases=alias_dict, 

206 prisma_client=prisma_client, 

207 ) 

208 await can_key_call_model( 

209 model=model, 

210 llm_model_list=None, 

211 valid_token=scoped_actor, 

212 llm_router=llm_router, 

213 prisma_client=prisma_client, 

214 ) 

215 await _check_team_member_model_access( 

216 model=model, 

217 team_object=team, 

218 valid_token=scoped_actor, 

219 llm_router=llm_router, 

220 prisma_client=None, 

221 user_api_key_cache=user_api_key_cache, 

222 proxy_logging_obj=proxy_logging_obj, 

223 team_membership=objects.membership, 

224 team_membership_loaded=True, 

225 ) 

226 if objects.organization is not None: 

227 can_org_access_model(model=model, org_object=objects.organization, llm_router=llm_router) 

228 if objects.project is not None: 

229 can_project_access_model(model=model, project_object=objects.project, llm_router=llm_router) 

230 

231 

232async def _load_member_auto_router_dependency_objects( 

233 *, user_api_key_dict: UserAPIKeyAuth, team: LiteLLM_TeamTable, prisma_client: DatabaseClient | None 

234) -> MemberAutoRouterDependencyObjects: 

235 if prisma_client is None: 

236 raise HTTPException(status_code=503, detail="Cannot verify auto-router model access without a database") 

237 membership_where: Final[_MembershipWhere] = { 

238 "user_id_team_id": {"user_id": user_api_key_dict.user_id or "", "team_id": team.team_id} 

239 } 

240 membership_include: Final[prisma_types.LiteLLM_TeamMembershipInclude] = {"litellm_budget_table": True} 

241 membership_row: Final = ( 

242 await TeamMembershipRepository(prisma_client).table.find_unique( 

243 where=membership_where, include=membership_include 

244 ) 

245 if user_api_key_dict.user_id 

246 else None 

247 ) 

248 membership: Final = ( 

249 LiteLLM_TeamMembership.model_validate(membership_row.model_dump()) if membership_row is not None else None 

250 ) 

251 organization: Final = ( 

252 await OrganizationRepository(prisma_client).find_by_id(team.organization_id) if team.organization_id else None 

253 ) 

254 if team.organization_id and organization is None: 

255 raise HTTPException(status_code=403, detail="The auto router's organization is unavailable.") 

256 project: Final = ( 

257 await ProjectRepository(prisma_client).find_by_id(user_api_key_dict.project_id) 

258 if user_api_key_dict.project_id 

259 else None 

260 ) 

261 return MemberAutoRouterDependencyObjects(membership=membership, organization=organization, project=project) 

262 

263 

264class StoredAutoRouterIdentity(BaseModel): 

265 created_by: str | None = None 

266 updated_at: datetime | None = None 

267 

268 

269@dataclass(frozen=True, slots=True) 

270class MemberAutoRouterWrite: 

271 actor: UserAPIKeyAuth 

272 team_id: str 

273 model_id: str | None 

274 public_name: str 

275 updated_at: datetime | None 

276 config: RequestComplexityRouterConfig 

277 default_model: str | None 

278 

279 

280async def authorize_member_auto_router_write( 

281 *, 

282 incoming: Deployment | updateDeployment, 

283 existing: Deployment | None, 

284 user_api_key_dict: UserAPIKeyAuth, 

285 team: LiteLLM_TeamTable, 

286 premium_user: bool, 

287 prisma_client: DatabaseClient, 

288 llm_router: Router, 

289) -> MemberAutoRouterWrite: 

290 authorize_member_auto_router_team(user_api_key_dict=user_api_key_dict, team=team, premium_user=premium_user) 

291 stored: Final = StoredAutoRouterIdentity.model_validate(existing.model_dump()) if existing is not None else None 

292 if stored is not None and stored.created_by != user_api_key_dict.user_id: 

293 raise HTTPException(status_code=403, detail="Team members can update only their own auto routers.") 

294 params: Final = incoming.litellm_params 

295 if params is None or incoming.model_fields_set - frozenset({"model_name", "litellm_params", "model_info"}): 

296 raise HTTPException(status_code=403, detail="Team members may change only auto-router configuration.") 

297 if params.model_fields_set - frozenset({"model", "complexity_router_config", "complexity_router_default_model"}): 

298 raise HTTPException(status_code=403, detail="Team members may change only auto-router configuration.") 

299 info: Final = incoming.model_info 

300 if info is not None and ( 

301 info.model_fields_set - frozenset({"id", "team_id"}) 

302 or info.team_id not in (None, team.team_id) 

303 or (existing is not None and "id" in info.model_fields_set and info.id != existing.model_info.id) 

304 ): 

305 raise HTTPException( 

306 status_code=403, detail="Team members cannot change model ownership or administrative settings." 

307 ) 

308 existing_model: Final = ( 

309 decrypt_value_helper(existing.litellm_params.model, key="model", return_original_value=True) 

310 if existing is not None 

311 else None 

312 ) 

313 effective_model: Final = params.model or existing_model 

314 if ( 

315 not isinstance(effective_model, str) 

316 or classify_strategy_router_model(effective_model) != "complexity" 

317 or (existing is not None and effective_model != existing_model) 

318 ): 

319 raise HTTPException(status_code=403, detail="Team members may manage only complexity auto routers.") 

320 public_name: Final = ( 

321 existing.model_info.team_public_model_name or existing.model_name 

322 if existing is not None 

323 else incoming.model_name 

324 ) 

325 if ( 

326 not public_name 

327 or public_name != public_name.strip() 

328 or any(character in public_name for character in "*?[]") 

329 or public_name.startswith("model_name_") 

330 ): 

331 raise HTTPException( 

332 status_code=400, detail="Choose a non-empty auto-router name without wildcards or internal prefixes." 

333 ) 

334 if existing is not None and incoming.model_name not in (None, public_name, existing.model_name): 

335 raise HTTPException(status_code=403, detail="Team members cannot rename an auto router.") 

336 supplied_config: Final = _RouterConfigSource.model_validate(params.model_dump()).complexity_router_config 

337 raw_config: Final = ( 

338 supplied_config 

339 if supplied_config is not None 

340 else _RouterConfigSource.model_validate(existing.litellm_params.model_dump()).complexity_router_config 

341 if existing is not None 

342 else None 

343 ) 

344 if raw_config is None: 

345 raise HTTPException(status_code=400, detail="A complexity_router_config is required.") 

346 config: Final = validate_member_auto_router_config(raw_config) 

347 stored_default: Final = existing.litellm_params.complexity_router_default_model if existing is not None else None 

348 default_model: Final = ( 

349 params.complexity_router_default_model 

350 if params.complexity_router_default_model is not None 

351 else decrypt_value_helper(stored_default, key="complexity_router_default_model", return_original_value=True) 

352 if stored_default is not None 

353 else None 

354 ) 

355 await authorize_member_auto_router_dependencies( 

356 config=config, 

357 default_model=default_model, 

358 user_api_key_dict=user_api_key_dict, 

359 team=team, 

360 prisma_client=prisma_client, 

361 llm_router=llm_router, 

362 ) 

363 return MemberAutoRouterWrite( 

364 actor=user_api_key_dict, 

365 team_id=team.team_id, 

366 model_id=existing.model_info.id if existing is not None else None, 

367 public_name=public_name, 

368 updated_at=stored.updated_at if stored is not None else None, 

369 config=config, 

370 default_model=default_model, 

371 )