chore(typing): clear basedpyright Any errors in proxy endpoints and http handlers

Replace Any-typed seams with concrete types across the proxy's highest
Any-count production files: typed FastAPI/aiohttp/DB helper signatures in
proxy_server.py (TypedDict for aiohttp connector kwargs, concrete route
and pydantic model types instead of Dict[str, Any]/kwargs-spread
constructors), llm_http_handler.py (logging_obj, encoding, provider
config parameters), proxy/utils.py and the key/team management
endpoints (CustomLogger/CustomGuardrail-typed callbacks, concrete
Prisma repository return types in place of raw .table access), and
streaming_handler.py (model_validate over dict-kwargs spreads).

Whole-tree basedpyright, same environment: reportExplicitAny
7283 -> 7228 (-55), reportAny 22710 -> 22217 (-493), with knock-on
drops across reportUnknownArgumentType, reportUnknownMemberType,
reportPrivateUsage, reportArgumentType, and reportUnnecessaryComparison.
Zero regressions in any rule. All mapped tests for the six touched
files pass.

Also satisfies the ruff-strict and type-discipline gates (Optional[X]
modernized to X | None, read-only DB-query results and route lists
typed Sequence/Mapping instead of List/Dict, and the handful of
genuinely in-place-mutated values marked # mutable-ok with a reason),
and ratchets down the ruff-strict, type-discipline, and basedpyright
lint budgets via make lint-budget-update to match.
This commit is contained in:
mateo-berri 2026-07-30 12:40:56 +00:00
parent 71b825a7f0
commit 68fb66c84e
No known key found for this signature in database
9 changed files with 222 additions and 144 deletions

View file

@ -1,15 +1,15 @@
{
"reportAny": {
"limit": 33171
"limit": 32185
},
"reportArgumentType": {
"limit": 2645
"limit": 2635
},
"reportAssignmentType": {
"limit": 329
},
"reportAttributeAccessIssue": {
"limit": 516
"limit": 514
},
"reportCallIssue": {
"limit": 123
@ -24,7 +24,7 @@
"limit": 42
},
"reportExplicitAny": {
"limit": 10228
"limit": 10118
},
"reportFunctionMemberAccess": {
"limit": 11
@ -54,10 +54,10 @@
"limit": 0
},
"reportMissingParameterType": {
"limit": 5869
"limit": 5867
},
"reportMissingTypeArgument": {
"limit": 15864
"limit": 15863
},
"reportMissingTypeStubs": {
"limit": 41
@ -84,7 +84,7 @@
"limit": 77
},
"reportPrivateUsage": {
"limit": 2437
"limit": 2415
},
"reportRedeclaration": {
"limit": 12
@ -99,25 +99,25 @@
"limit": 0
},
"reportUnknownArgumentType": {
"limit": 45522
"limit": 45254
},
"reportUnknownLambdaType": {
"limit": 113
},
"reportUnknownMemberType": {
"limit": 40479
"limit": 40440
},
"reportUnknownParameterType": {
"limit": 20341
"limit": 20338
},
"reportUnknownVariableType": {
"limit": 32052
"limit": 32048
},
"reportUnnecessaryCast": {
"limit": 177
},
"reportUnnecessaryComparison": {
"limit": 1023
"limit": 1015
},
"reportUnnecessaryContains": {
"limit": 7

View file

@ -30,7 +30,6 @@ from litellm._uuid import uuid
from litellm.litellm_core_utils.model_response_utils import (
is_model_response_stream_empty,
)
from litellm.litellm_core_utils.redact_messages import LiteLLMLoggingObject
from litellm.litellm_core_utils.thread_pool_executor import executor
from litellm.types.llms.openai import OpenAIChatCompletionChunk
from litellm.types.router import GenericLiteLLMParams
@ -125,14 +124,17 @@ class CustomStreamWrapper:
self.model = model
self.make_call = make_call
self.custom_llm_provider = custom_llm_provider
self.logging_obj: LiteLLMLoggingObject = logging_obj
self.logging_obj: Any = logging_obj
self.completion_stream = completion_stream
self.sent_first_chunk = False
self.sent_last_chunk = False
self._stream_created_time: float = time.time()
litellm_params: GenericLiteLLMParams = GenericLiteLLMParams(
**self.logging_obj.model_call_details.get("litellm_params", {})
_litellm_params_kwargs = self.logging_obj.model_call_details.get("litellm_params", {})
litellm_params: GenericLiteLLMParams = (
GenericLiteLLMParams.model_validate(_litellm_params_kwargs)
if isinstance(_litellm_params_kwargs, dict)
else GenericLiteLLMParams()
)
self.merge_reasoning_content_in_choices: bool = litellm_params.merge_reasoning_content_in_choices or False
self.sent_first_thinking_block = False
@ -677,7 +679,7 @@ class CustomStreamWrapper:
if chunk:
args.update({k: v for k, v in chunk.items() if k != "stream"})
model_response = ModelResponseStream(**args)
model_response = ModelResponseStream.model_validate(args)
if self.response_id is not None:
model_response.id = self.response_id
if self.system_fingerprint is not None:
@ -817,7 +819,7 @@ class CustomStreamWrapper:
_initial_delta = model_response.choices[0].delta.model_dump()
_initial_delta.pop("role", None)
model_response.choices[0].delta = Delta(**_initial_delta)
model_response.choices[0].delta = Delta.model_validate(_initial_delta)
return model_response
def _has_special_delta_content(self, model_response: ModelResponseStream) -> bool:
@ -915,7 +917,7 @@ class CustomStreamWrapper:
choice_json.pop(
"finish_reason", None
) # for mistral etc. which return a value in their last chunk (not-openai compatible).
choices.append(StreamingChoices(**choice_json))
choices.append(StreamingChoices.model_validate(choice_json))
except Exception:
choices.append(StreamingChoices())
setattr(model_response, "choices", choices)
@ -946,7 +948,7 @@ class CustomStreamWrapper:
self.sent_first_chunk = True
if response_obj.get("provider_specific_fields") is not None:
completion_obj["provider_specific_fields"] = response_obj["provider_specific_fields"]
model_response.choices[0].delta = Delta(**completion_obj)
model_response.choices[0].delta = Delta.model_validate(completion_obj)
_index: Optional[int] = completion_obj.get("index")
if _index is not None:
model_response.choices[0].index = _index

View file

@ -160,8 +160,10 @@ def _rust_responses_websocket_enabled(
from .http_handler import get_shared_realtime_ssl_context
if TYPE_CHECKING:
import tiktoken
from aiohttp import ClientSession
from litellm.integrations.custom_logger import CustomLogger
from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj
from litellm.llms.anthropic.experimental_pass_through.messages.streaming_iterator import (
AnthropicMessagesStreamingResponse,
@ -210,7 +212,7 @@ def _responses_api_optional_request_param_names() -> frozenset[str]:
return frozenset(get_type_hints(ResponsesAPIOptionalRequestParams).keys())
def _custom_logger_callbacks(logging_obj: Any) -> list[Any]:
def _custom_logger_callbacks(logging_obj: LiteLLMLoggingObj) -> tuple["CustomLogger", ...]:
from litellm.integrations.custom_logger import CustomLogger
from litellm.litellm_core_utils.litellm_logging import (
get_custom_logger_compatible_class,
@ -221,7 +223,7 @@ def _custom_logger_callbacks(logging_obj: Any) -> list[Any]:
if isinstance(dynamic_success_callbacks, (list, tuple)):
callbacks.extend(dynamic_success_callbacks)
custom_loggers: list[Any] = []
custom_loggers = []
for cb in callbacks:
if isinstance(cb, str):
resolved = get_custom_logger_compatible_class(cb) # type: ignore[arg-type]
@ -230,10 +232,10 @@ def _custom_logger_callbacks(logging_obj: Any) -> list[Any]:
cb = resolved
if isinstance(cb, CustomLogger):
custom_loggers.append(cb)
return custom_loggers
return tuple(custom_loggers)
def _has_pre_call_deployment_hook(logging_obj: Any) -> bool:
def _has_pre_call_deployment_hook(logging_obj: LiteLLMLoggingObj) -> bool:
from litellm.integrations.custom_logger import CustomLogger
base_func = CustomLogger.async_pre_call_deployment_hook
@ -359,7 +361,7 @@ class BaseLLMHTTPHandler:
messages: list,
optional_params: dict,
litellm_params: dict,
encoding: Any,
encoding: Optional["tiktoken.Encoding"],
api_key: Optional[str] = None,
client: Optional[AsyncHTTPHandler] = None,
json_mode: bool = False,
@ -425,7 +427,7 @@ class BaseLLMHTTPHandler:
api_base: Optional[str],
custom_llm_provider: str,
model_response: ModelResponse,
encoding,
encoding: Optional["tiktoken.Encoding"],
logging_obj: LiteLLMLoggingObj,
optional_params: dict,
timeout: Union[float, httpx.Timeout],
@ -5008,7 +5010,7 @@ class BaseLLMHTTPHandler:
return depth, max(max_loops, 1), fingerprints
@staticmethod
def _has_agentic_completion_hook(logging_obj: Any) -> bool:
def _has_agentic_completion_hook(logging_obj: LiteLLMLoggingObj) -> bool:
"""
True if any registered callback actually overrides
``async_should_run_agentic_loop`` (the gate every agentic hook goes
@ -5248,7 +5250,7 @@ class BaseLLMHTTPHandler:
self,
result: Any,
model: str,
responses_api_provider_config: Any,
responses_api_provider_config: BaseResponsesAPIConfig,
logging_obj: "LiteLLMLoggingObj",
custom_llm_provider: str,
) -> Any:
@ -5769,7 +5771,7 @@ class BaseLLMHTTPHandler:
websockets_module: Any,
url: str,
headers: dict,
ssl_context: Any,
ssl_context: bool | str | ssl.SSLContext | None,
*,
open_timeout: float = 8.0,
max_attempts: int = 3,

View file

@ -490,7 +490,7 @@ _NON_ADMIN_SAFE_ALLOWED_ROUTES_PRESETS = frozenset({"llm_api_routes", "info_rout
def _validate_caller_can_change_key_ownership(
data: Optional[BaseModel],
existing_key_row: Any,
existing_key_row: LiteLLM_VerificationToken,
user_api_key_dict: UserAPIKeyAuth,
) -> None:
"""
@ -523,7 +523,7 @@ def _validate_caller_can_change_key_ownership(
status_code=403,
detail="Non-admin users cannot remove the user_id from a key.",
)
existing_user_id = getattr(existing_key_row, "user_id", None)
existing_user_id = existing_key_row.user_id
if incoming_user_id != existing_user_id:
raise HTTPException(
status_code=403,
@ -2112,7 +2112,7 @@ async def _process_single_key_update(
litellm_changed_by: Optional[str],
prisma_client: Optional[PrismaClient],
user_api_key_cache: UserApiKeyCache,
proxy_logging_obj: Any,
proxy_logging_obj: ProxyLogging,
llm_router: Optional[Router],
user_custom_key_update: Optional[Callable] = None,
existing_key_row: Optional[LiteLLM_VerificationToken] = None,
@ -2265,9 +2265,9 @@ async def _process_single_key_update(
async def _validate_mcp_servers_for_key_update(
data: "UpdateKeyRequest",
team_obj: Optional["LiteLLM_TeamTableCachedObj"],
existing_key_row: Any,
prisma_client: Any,
user_api_key_cache: Any,
existing_key_row: LiteLLM_VerificationToken,
prisma_client: PrismaClient | None,
user_api_key_cache: UserApiKeyCache,
is_proxy_admin: bool,
) -> Optional[ObjectPermissionDict]:
"""Validate MCP servers in object_permission against the effective team."""
@ -2302,14 +2302,20 @@ async def _validate_mcp_servers_for_key_update(
async def _validate_update_key_data(
data: UpdateKeyRequest,
existing_key_row: Any,
existing_key_row: LiteLLM_VerificationToken,
user_api_key_dict: UserAPIKeyAuth,
llm_router: Any,
llm_router: Router | None,
premium_user: bool,
prisma_client: Any,
user_api_key_cache: Any,
prisma_client: PrismaClient | None,
user_api_key_cache: UserApiKeyCache,
) -> None:
"""Validate permissions and constraints for key update."""
if prisma_client is None:
raise HTTPException(
status_code=500,
detail={"error": "Database not connected"},
)
# Reject NaN/±inf spend before it can reach the DB / spend counter.
validate_finite_spend(data.spend)
@ -2414,11 +2420,12 @@ async def _validate_update_key_data(
# _check_key_admin_access that would otherwise require team/org admin status.
_key_is_team_key = getattr(existing_key_row, "team_id", None) is not None
can_skip_admin_check = (caller_is_creator or _key_is_team_key) and not _is_budget_change
if (not _is_proxy_admin) and prisma_client is not None and not can_skip_admin_check:
hashed_key = existing_key_row.token
if (not _is_proxy_admin) and not can_skip_admin_check:
if existing_key_row.token is None:
raise HTTPException(status_code=500, detail={"error": "Existing key is missing its token"})
await _check_key_admin_access(
user_api_key_dict=user_api_key_dict,
hashed_token=hashed_key,
hashed_token=existing_key_row.token,
prisma_client=prisma_client,
user_api_key_cache=user_api_key_cache,
route=("/key/update (max_budget/spend)" if _is_budget_change else "/key/update"),
@ -5421,7 +5428,7 @@ async def list_keys(
async def _apply_non_admin_alias_scope(
user_api_key_dict: UserAPIKeyAuth,
prisma_client: Any,
prisma_client: PrismaClient | None,
query_params: List[Any],
where_parts: List[str],
) -> None:
@ -5852,9 +5859,7 @@ async def _list_key_helper(
where=where # type: ignore
)
else:
total_count = await VerificationTokenRepository(prisma_client).table.count(
where=where # type: ignore
)
total_count = await VerificationTokenRepository(prisma_client).count(where=where)
verbose_proxy_logger.debug(f"Total count of keys: {total_count}")
@ -5932,7 +5937,7 @@ def _get_condition_to_filter_out_ui_session_tokens() -> Dict[str, Any]:
async def _check_key_admin_access(
user_api_key_dict: UserAPIKeyAuth,
hashed_token: str,
prisma_client: Any,
prisma_client: PrismaClient | None,
user_api_key_cache: UserApiKeyCache,
route: str,
) -> None:
@ -6443,7 +6448,7 @@ def _validate_key_alias_format(key_alias: Optional[str]) -> None:
async def _enforce_unique_key_alias(
key_alias: Optional[str],
prisma_client: Any,
prisma_client: PrismaClient | None,
existing_key_token: Optional[str] = None,
) -> None:
"""
@ -6451,7 +6456,7 @@ async def _enforce_unique_key_alias(
Args:
key_alias (Optional[str]): The key alias to check
prisma_client (Any): Prisma client instance
prisma_client (Optional[PrismaClient]): Prisma client instance
existing_key_token (Optional[str]): ID of existing key being updated, to exclude from uniqueness check
(The Admin UI passes key_alias, in all Edit key requests. So we need to be sure that if we find a key with the same alias, it's not the same key we're updating)

View file

@ -14,7 +14,7 @@ import json
import math
import traceback
from datetime import datetime, timezone
from typing import Annotated, Any, Dict, List, Mapping, Optional, Tuple, Union, cast
from typing import Annotated, Any, Dict, List, Mapping, Optional, Sequence, Tuple, Union, cast
import fastapi
from fastapi import APIRouter, Depends, Header, HTTPException, Request, status
@ -31,6 +31,7 @@ from litellm.proxy._types import (
CommonProxyErrors,
DeleteTeamRequest,
LiteLLM_AuditLogs,
LiteLLM_BudgetTable,
LiteLLM_DeletedTeamTable,
LiteLLM_ManagementEndpoint_MetadataFields,
LiteLLM_ManagementEndpoint_MetadataFields_Premium,
@ -78,6 +79,7 @@ from litellm.proxy.auth.auth_checks import (
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.proxy.common_utils.callback_utils import encrypt_callback_vars
from litellm.proxy.common_utils.json_merge_patch import apply_json_merge_patch
from litellm.proxy.common_utils.user_api_key_cache import UserApiKeyCache
from litellm.proxy.management_endpoints.common_utils import (
_check_passthrough_routes_caller_permission,
_is_user_org_admin_for_team,
@ -106,7 +108,7 @@ from litellm.proxy.management_helpers.utils import (
add_new_member,
management_endpoint_wrapper,
)
from litellm.proxy.utils import PrismaClient, handle_exception_on_proxy
from litellm.proxy.utils import PrismaClient, ProxyLogging, handle_exception_on_proxy
from litellm.repositories.budget_repository import BudgetRepository
from litellm.repositories.organization_repository import OrganizationRepository
from litellm.repositories.table_repositories import (
@ -151,9 +153,9 @@ def _sanitize_for_log(value: object) -> str:
async def _refresh_cached_team(
team_row: Any,
user_api_key_cache: Any,
proxy_logging_obj: Any,
team_row: LiteLLM_TeamTable,
user_api_key_cache: UserApiKeyCache,
proxy_logging_obj: ProxyLogging,
) -> None:
"""
Refresh the in-memory cached team object after a DB write.
@ -274,7 +276,7 @@ class TeamMemberBudgetHandler:
if team_member_budget_duration is not None:
budget_request.budget_duration = team_member_budget_duration
team_member_budget_table = await new_budget(
team_member_budget_table: LiteLLM_BudgetTable = await new_budget(
budget_obj=budget_request,
user_api_key_dict=user_api_key_dict,
)
@ -322,7 +324,7 @@ class TeamMemberBudgetHandler:
if team_member_budget_duration is not None:
budget_request.budget_duration = team_member_budget_duration
budget_row = await update_budget(
budget_row: LiteLLM_BudgetTable = await update_budget(
budget_obj=budget_request,
user_api_key_dict=user_api_key_dict,
)
@ -395,7 +397,7 @@ class TeamMemberBudgetHandler:
@staticmethod
async def backfill_team_member_budget_entries(
team_id: str,
members_with_roles: List[Union[Member, dict]],
members_with_roles: Sequence[Union[Member, dict]],
team_member_budget_id: str,
prisma_client: PrismaClient,
) -> None:
@ -414,7 +416,9 @@ class TeamMemberBudgetHandler:
return
# Batch-fetch existing memberships for this team (avoids N+1 queries)
existing_memberships = await TeamMembershipRepository(prisma_client).table.find_many(where={"team_id": team_id})
existing_memberships: Sequence[LiteLLM_TeamMembership] = await TeamMembershipRepository(
prisma_client
).table.find_many(where={"team_id": team_id})
existing_user_ids = {m.user_id for m in existing_memberships}
# Identify members with no existing membership row.
@ -447,7 +451,7 @@ class TeamMemberBudgetHandler:
# Heal existing membership rows that predate the team_member_budget
# configuration: populate budget_id where it is currently NULL.
# Rows with an explicit budget_id (per-member override) are left alone.
updated = await TeamMembershipRepository(prisma_client).table.update_many(
updated: int = await TeamMembershipRepository(prisma_client).table.update_many(
where={"team_id": team_id, "budget_id": None},
data={"budget_id": team_member_budget_id},
)
@ -503,7 +507,7 @@ async def get_all_team_memberships(
# else:
# where_obj = {"user_id": str(user_id), "team_id": {"in": team_id}}
team_memberships = await TeamMembershipRepository(prisma_client).table.find_many(
team_memberships: Sequence[LiteLLM_TeamMembership] = await TeamMembershipRepository(prisma_client).table.find_many(
where=where_obj,
include={"litellm_budget_table": True},
)
@ -765,7 +769,7 @@ async def _check_org_team_limits(
# calculate allocated tpm/rpm limit
# check if specified tpm/rpm limit is greater than allocated tpm/rpm limit
teams = await TeamRepository(prisma_client).table.find_many(
teams: Sequence[LiteLLM_TeamTable] = await TeamRepository(prisma_client).table.find_many(
where={"organization_id": org_table.organization_id},
)
@ -790,7 +794,7 @@ async def _check_user_team_limits(
data: Union[NewTeamRequest, UpdateTeamRequest],
user_api_key_dict: UserAPIKeyAuth,
prisma_client: PrismaClient,
user_api_key_cache: Any,
user_api_key_cache: UserApiKeyCache,
) -> None:
"""
Enforce the caller's personal limits when CREATING a standalone team.
@ -1394,7 +1398,7 @@ async def _update_model_table(
async def _auto_add_team_members_to_organization(
team: LiteLLM_TeamTable,
organization: LiteLLM_OrganizationTableWithMembers,
prisma_client: Any,
prisma_client: PrismaClient,
) -> None:
"""
When moving a team to an org, ensure all team members are also org members.
@ -1432,11 +1436,11 @@ async def _auto_add_team_members_to_organization(
async def fetch_and_validate_organization(
organization_id: str,
existing_team_row: Any,
existing_team_row: LiteLLM_TeamTable,
llm_router: Optional[Router],
prisma_client: Any,
prisma_client: PrismaClient,
user_api_key_dict: Optional[UserAPIKeyAuth] = None,
) -> Any:
) -> LiteLLM_OrganizationTable | None:
"""
Fetch and validate an organization for team update operations.
@ -1455,7 +1459,7 @@ async def fetch_and_validate_organization(
if llm_router is None:
raise HTTPException(status_code=500, detail={"error": CommonProxyErrors.no_llm_router.value})
organization_row = await OrganizationRepository(prisma_client).table.find_unique(
organization_row: LiteLLM_OrganizationTable | None = await OrganizationRepository(prisma_client).table.find_unique(
where={"organization_id": organization_id},
include={"litellm_budget_table": True, "members": True, "teams": True},
)
@ -1704,7 +1708,9 @@ async def update_team(
detail={"error": f"soft_budget must be a non-negative finite number. Received: {data.soft_budget}"},
)
existing_team_row = await TeamRepository(prisma_client).table.find_unique(where={"team_id": data.team_id})
existing_team_row: LiteLLM_TeamTable | None = await TeamRepository(prisma_client).table.find_unique(
where={"team_id": data.team_id}
)
if existing_team_row is None:
raise HTTPException(
@ -1792,7 +1798,9 @@ async def update_team(
# check org team limits - if updating team that belongs to an org
org_id_to_check = (
data.organization_id if data.organization_id is not None else existing_team_row.organization_id
data.organization_id
if data.organization_id is not None
else getattr(existing_team_row, "organization_id", None)
)
if org_id_to_check is not None and isinstance(org_id_to_check, str) and prisma_client is not None:
org_table = await get_org_object(
@ -1895,7 +1903,7 @@ async def update_team(
updated_kv.pop("model_aliases")
_model_id = await _update_model_table(
data=data,
model_id=existing_team_row.model_id,
model_id=getattr(existing_team_row, "model_id", None),
prisma_client=prisma_client,
user_api_key_dict=user_api_key_dict,
litellm_proxy_admin_name=litellm_proxy_admin_name,
@ -2004,13 +2012,16 @@ async def patch_team(
patch_fields = data.model_dump(exclude_unset=True, exclude={"team_id"})
if "metadata" in patch_fields:
existing_team_row = await TeamRepository(prisma_client).table.find_unique(where={"team_id": team_id})
existing_team_row: LiteLLM_TeamTable | None = await TeamRepository(prisma_client).table.find_unique(
where={"team_id": team_id}
)
if existing_team_row is None:
raise HTTPException(
status_code=404,
detail={"error": f"Team not found, passed team_id={team_id}"},
)
existing_metadata = existing_team_row.metadata if isinstance(existing_team_row.metadata, dict) else {}
_existing_metadata = getattr(existing_team_row, "metadata", None)
existing_metadata = _existing_metadata if isinstance(_existing_metadata, dict) else {}
patch_fields["metadata"] = apply_json_merge_patch(existing_metadata, patch_fields["metadata"])
update_request = UpdateTeamRequest.model_validate({"team_id": team_id, **patch_fields})
@ -2706,7 +2717,9 @@ async def team_member_delete(
detail={"error": "Either user_id or user_email needs to be passed in"},
)
_existing_team_row = await TeamRepository(prisma_client).table.find_unique(where={"team_id": data.team_id})
_existing_team_row: LiteLLM_TeamTable | None = await TeamRepository(prisma_client).table.find_unique(
where={"team_id": data.team_id}
)
if _existing_team_row is None:
raise HTTPException(
@ -2910,7 +2923,9 @@ async def team_member_update(
_validate_budget_duration(data.budget_duration)
_existing_team_row = await TeamRepository(prisma_client).table.find_unique(where={"team_id": data.team_id})
_existing_team_row: LiteLLM_TeamTable | None = await TeamRepository(prisma_client).table.find_unique(
where={"team_id": data.team_id}
)
if _existing_team_row is None:
raise HTTPException(
@ -3818,7 +3833,9 @@ async def block_team(
if prisma_client is None:
raise Exception("No DB Connected.")
existing_team = await TeamRepository(prisma_client).table.find_unique(where={"team_id": data.team_id})
existing_team: LiteLLM_TeamTable | None = await TeamRepository(prisma_client).table.find_unique(
where={"team_id": data.team_id}
)
if existing_team is None:
raise HTTPException(
status_code=404,
@ -3831,7 +3848,7 @@ async def block_team(
user_api_key_dict=user_api_key_dict,
)
record = await TeamRepository(prisma_client).table.update(
record: LiteLLM_TeamTable | None = await TeamRepository(prisma_client).table.update(
where={"team_id": data.team_id},
data={"blocked": True}, # type: ignore
)
@ -3867,7 +3884,9 @@ async def unblock_team(
if prisma_client is None:
raise Exception("No DB Connected.")
existing_team = await TeamRepository(prisma_client).table.find_unique(where={"team_id": data.team_id})
existing_team: LiteLLM_TeamTable | None = await TeamRepository(prisma_client).table.find_unique(
where={"team_id": data.team_id}
)
if existing_team is None:
raise HTTPException(
status_code=404,
@ -3880,7 +3899,7 @@ async def unblock_team(
user_api_key_dict=user_api_key_dict,
)
record = await TeamRepository(prisma_client).table.update(
record: LiteLLM_TeamTable | None = await TeamRepository(prisma_client).table.update(
where={"team_id": data.team_id},
data={"blocked": False}, # type: ignore
)
@ -3914,7 +3933,9 @@ async def list_available_teams(
return []
# filter out teams that the user is already a member of
user_info = await UserRepository(prisma_client).table.find_unique(where={"user_id": user_api_key_dict.user_id})
user_info: LiteLLM_UserTable | None = await UserRepository(prisma_client).table.find_unique(
where={"user_id": user_api_key_dict.user_id}
)
if user_info is None:
raise HTTPException(
status_code=404,
@ -3924,7 +3945,9 @@ async def list_available_teams(
available_teams = [team for team in available_teams if team not in user_info_correct_type.teams]
available_teams_db = await TeamRepository(prisma_client).table.find_many(where={"team_id": {"in": available_teams}})
available_teams_db: Sequence[LiteLLM_TeamTable] = await TeamRepository(prisma_client).table.find_many(
where={"team_id": {"in": available_teams}}
)
available_teams_correct_type = [LiteLLM_TeamTable.model_validate(team.model_dump()) for team in available_teams_db]
@ -3933,9 +3956,9 @@ async def list_available_teams(
async def _get_org_admin_org_ids(
user_id: str,
prisma_client: Any,
user_api_key_cache: Any,
proxy_logging_obj: Any,
prisma_client: PrismaClient,
user_api_key_cache: UserApiKeyCache,
proxy_logging_obj: ProxyLogging,
) -> Optional[List[str]]:
"""
Return the list of organization IDs where the user is an org admin.
@ -4113,7 +4136,7 @@ def _convert_teams_to_response_models(
async def _get_keys_count_by_team(
prisma_client: Any,
prisma_client: PrismaClient,
teams: list,
) -> Dict[str, int]:
"""Aggregate virtual-key counts per team for the given page of teams.
@ -4138,9 +4161,9 @@ async def _enforce_list_team_v2_access(
user_api_key_dict: UserAPIKeyAuth,
user_id: Optional[str],
organization_id: Optional[str],
prisma_client: Any,
user_api_key_cache: Any,
proxy_logging_obj: Any,
prisma_client: PrismaClient,
user_api_key_cache: UserApiKeyCache,
proxy_logging_obj: ProxyLogging,
) -> Tuple[Optional[str], Optional[List[str]]]:
"""Enforce access control for list_team_v2.
@ -4391,9 +4414,9 @@ async def list_team_v2(
async def _authorize_and_filter_teams(
user_api_key_dict: UserAPIKeyAuth,
user_id: Optional[str],
prisma_client: Any,
user_api_key_cache: Any,
proxy_logging_obj: Any,
prisma_client: PrismaClient,
user_api_key_cache: UserApiKeyCache,
proxy_logging_obj: ProxyLogging,
) -> list:
"""
Authorize the /team/list request and return filtered teams.
@ -4568,7 +4591,9 @@ async def get_paginated_teams(
total_count = await TeamRepository(prisma_client).table.count()
# Get paginated teams
teams = await TeamRepository(prisma_client).table.find_many(
teams: list[LiteLLM_TeamTable] = await TeamRepository( # mutable-ok: matches this function's list return type
prisma_client
).table.find_many(
skip=skip,
take=page_size,
order={"team_alias": "asc"}, # Sort by team_alias
@ -4633,7 +4658,7 @@ async def ui_view_teams(
}
# Query users with pagination and filters
teams = await TeamRepository(prisma_client).table.find_many(
teams: Sequence[LiteLLM_TeamTable] = await TeamRepository(prisma_client).table.find_many(
where=where_conditions,
skip=skip,
take=page_size,
@ -4701,7 +4726,9 @@ async def team_model_add(
raise HTTPException(status_code=500, detail={"error": "No db connected"})
# Get existing team
team_row = await TeamRepository(prisma_client).table.find_unique(where={"team_id": data.team_id})
team_row: LiteLLM_TeamTable | None = await TeamRepository(prisma_client).table.find_unique(
where={"team_id": data.team_id}
)
if team_row is None:
raise HTTPException(
@ -4747,11 +4774,16 @@ async def team_model_add(
# the writer and lets Prisma bump updated_at.
# `include` mirrors the relations the auth path consumes off the cached
# team object so that `_refresh_cached_team` doesn't null them out.
updated_team = await TeamRepository(prisma_client).table.update(
updated_team: LiteLLM_TeamTable | None = await TeamRepository(prisma_client).table.update(
where={"team_id": data.team_id},
data={"updated_at": datetime.now(timezone.utc)},
include={"object_permission": True}, # type: ignore
)
if updated_team is None:
raise HTTPException(
status_code=404,
detail={"error": f"Team not found, passed team_id={data.team_id}"},
)
await _refresh_cached_team(
team_row=updated_team,
@ -4801,7 +4833,9 @@ async def team_model_delete(
raise HTTPException(status_code=500, detail={"error": "No db connected"})
# Get existing team
team_row = await TeamRepository(prisma_client).table.find_unique(where={"team_id": data.team_id})
team_row: LiteLLM_TeamTable | None = await TeamRepository(prisma_client).table.find_unique(
where={"team_id": data.team_id}
)
if team_row is None:
raise HTTPException(
@ -4829,11 +4863,16 @@ async def team_model_delete(
updated_models = [m for m in current_models if m not in data.models]
# Update team. See team_model_add for the rationale on `include`.
updated_team = await TeamRepository(prisma_client).table.update(
updated_team: LiteLLM_TeamTable | None = await TeamRepository(prisma_client).table.update(
where={"team_id": data.team_id},
data={"models": updated_models},
include={"object_permission": True}, # type: ignore
)
if updated_team is None:
raise HTTPException(
status_code=404,
detail={"error": f"Team not found, passed team_id={data.team_id}"},
)
await _refresh_cached_team(
team_row=updated_team,
@ -4964,10 +5003,15 @@ async def update_team_member_permissions(
},
)
# Update the team member permissions
updated_team = await TeamRepository(prisma_client).table.update(
updated_team: LiteLLM_TeamTable | None = await TeamRepository(prisma_client).table.update(
where={"team_id": data.team_id},
data={"team_member_permissions": data.team_member_permissions},
)
if updated_team is None:
raise HTTPException(
status_code=404,
detail={"error": f"Team not found, passed team_id={data.team_id}"},
)
return updated_team
@ -5058,7 +5102,7 @@ async def _compute_and_batch_updates(prisma_client, teams, permissions_to_add: s
async def _append_permissions_to_specific_teams(prisma_client, team_ids: List[str], permissions_to_add: set) -> int:
"""Fetch specific teams by ID and append permissions."""
teams = await TeamRepository(prisma_client).table.find_many(
teams: Sequence[LiteLLM_TeamTable] = await TeamRepository(prisma_client).table.find_many(
where={"team_id": {"in": team_ids}},
)
@ -5088,7 +5132,7 @@ async def _append_permissions_to_all_teams(prisma_client, permissions_to_add: se
find_args["cursor"] = {"team_id": cursor}
find_args["skip"] = 1
teams = await TeamRepository(prisma_client).table.find_many(**find_args)
teams: Sequence[LiteLLM_TeamTable] = await TeamRepository(prisma_client).table.find_many(**find_args)
if not teams:
break
@ -5188,8 +5232,12 @@ async def get_team_daily_activity(
where_condition = {}
if team_ids_list:
where_condition["team_id"] = {"in": list(team_ids_list)}
team_aliases = await TeamRepository(prisma_client).table.find_many(where=where_condition)
team_alias_metadata = {t.team_id: {"team_alias": t.team_alias} for t in team_aliases}
team_aliases: Sequence[LiteLLM_TeamTable] = await TeamRepository(prisma_client).table.find_many(
where=where_condition
)
team_alias_metadata: Mapping[str, dict[str, object]] = {
t.team_id: {"team_alias": t.team_alias} for t in team_aliases
}
# Check if user is team admin or has /team/daily/activity permission
# If not, filter by user's API keys.
@ -5219,9 +5267,9 @@ async def get_team_daily_activity(
# If user does not have full team view, filter by their API keys
if not has_full_team_view:
# Get all API keys for this user
user_keys = await VerificationTokenRepository(prisma_client).table.find_many(
where={"user_id": user_api_key_dict.user_id}
)
user_keys: Sequence[LiteLLM_VerificationToken] = await VerificationTokenRepository(
prisma_client
).table.find_many(where={"user_id": user_api_key_dict.user_id})
user_api_keys = [key.token for key in user_keys if key.token]
# If user has no API keys, return empty result
if not user_api_keys:

View file

@ -26,6 +26,7 @@ from typing import (
List,
Literal,
Optional,
Sequence,
Set,
Tuple,
TypedDict,
@ -128,7 +129,7 @@ from litellm.utils import (
)
if TYPE_CHECKING:
from aiohttp import ClientSession
from aiohttp import ClientSession, SocketFactoryType
from opentelemetry.trace import Span as _Span
from litellm.integrations.opentelemetry import OpenTelemetry
@ -648,7 +649,7 @@ from fastapi.responses import (
RedirectResponse,
StreamingResponse,
)
from fastapi.routing import APIRouter
from fastapi.routing import APIRoute, APIRouter
from fastapi.security import OAuth2PasswordBearer
from fastapi.security.api_key import APIKeyHeader
from fastapi.staticfiles import StaticFiles
@ -830,6 +831,15 @@ async def proxy_shutdown_event():
cleanup_router_config_variables()
class _AiohttpConnectorKwargs(TypedDict, total=False):
keepalive_timeout: int
ttl_dns_cache: int
enable_cleanup_closed: bool
limit: int
limit_per_host: int
socket_factory: "SocketFactoryType"
async def _initialize_shared_aiohttp_session():
"""Initialize shared aiohttp session for connection reuse with connection limits."""
try:
@ -839,7 +849,7 @@ async def _initialize_shared_aiohttp_session():
_build_aiohttp_keepalive_socket_factory,
)
connector_kwargs: Dict[str, Any] = {
connector_kwargs: _AiohttpConnectorKwargs = {
"keepalive_timeout": AIOHTTP_KEEPALIVE_TIMEOUT,
"ttl_dns_cache": AIOHTTP_TTL_DNS_CACHE,
}
@ -1153,7 +1163,7 @@ async def proxy_startup_event(app: FastAPI):
await proxy_shutdown_event() # type: ignore[reportGeneralTypeIssues]
def _generate_stable_operation_id(route: Any) -> str:
def _generate_stable_operation_id(route: APIRoute) -> str:
operation_id = re.sub(r"\W", "_", f"{route.name}{route.path_format}")
route_methods = sorted(route.methods or [])
if len(route_methods) == 1:
@ -1267,7 +1277,10 @@ vertex_live_passthrough_vertex_base = VertexBase()
from fastapi.routing import APIWebSocketRoute
def _inject_websocket_stubs_into_openapi_schema(openapi_schema: dict, websocket_routes: list) -> dict:
def _inject_websocket_stubs_into_openapi_schema(
openapi_schema: dict, # mutable-ok: mutated in place and returned to the caller
websocket_routes: Sequence[APIWebSocketRoute],
) -> dict: # mutable-ok: same object as the openapi_schema argument, mutated in place
"""
Add a synthetic GET stub for each WebSocket route so it appears in Swagger UI.
@ -1281,7 +1294,7 @@ def _inject_websocket_stubs_into_openapi_schema(openapi_schema: dict, websocket_
parameters = []
try:
if hasattr(route, "dependant") and route.dependant is not None:
if hasattr(route, "dependant"):
# Handle both FastAPI <0.120 and >=0.120
query_params = getattr(route.dependant, "query_params", [])
if query_params:
@ -3518,7 +3531,7 @@ _DB_OVERLAY_REMOTE_MODULE_LIST_FIELDS: Dict[str, Tuple[str, ...]] = {
}
def _is_remote_module_url(value: Any) -> bool:
def _is_remote_module_url(value: object) -> bool:
return isinstance(value, str) and (value.startswith("s3://") or value.startswith("gcs://"))
@ -5398,7 +5411,7 @@ class ProxyConfig:
# decrypt values
for k, v in _litellm_params.items():
_litellm_params[k] = self._resolve_db_litellm_param(key=k, value=v)
_litellm_params = LiteLLM_Params(**_litellm_params)
_litellm_params = LiteLLM_Params.model_validate(_litellm_params)
else:
verbose_proxy_logger.error(
@ -5429,7 +5442,7 @@ class ProxyConfig:
# decrypt values
for k, v in _litellm_params.items():
_litellm_params[k] = self._resolve_db_litellm_param(key=k, value=v)
_litellm_params = LiteLLM_Params(**_litellm_params)
_litellm_params = LiteLLM_Params.model_validate(_litellm_params)
else:
verbose_proxy_logger.error(
f"Invalid model added to proxy db. Invalid litellm params. litellm_params={_litellm_params}"
@ -6897,7 +6910,7 @@ class ProxyConfig:
if isinstance(credential, dict):
credential_object = CredentialItem(**credential)
elif isinstance(credential, BaseModel):
credential_object = CredentialItem(**credential.model_dump())
credential_object = CredentialItem.model_validate(credential.model_dump())
decrypted_credential_values = {}
for k, v in credential_object.credential_values.items():
@ -13063,7 +13076,7 @@ def _get_model_group_info(
_model_group_info = llm_router.get_model_group_info(model_group=model)
if _model_group_info is not None:
model_groups.append(ModelGroupInfoProxy(**_model_group_info.model_dump()))
model_groups.append(ModelGroupInfoProxy.model_validate(_model_group_info.model_dump()))
else:
model_group_info = ModelGroupInfoProxy(
model_group=model,
@ -14782,7 +14795,7 @@ async def update_config_general_settings(
)
try:
ConfigGeneralSettings(**{data.field_name: data.field_value})
ConfigGeneralSettings.model_validate({data.field_name: data.field_value})
except Exception:
raise HTTPException(
status_code=400,

View file

@ -345,7 +345,7 @@ def _accepts_litellm_call_info(cb: CustomLogger) -> bool:
return _CALLBACK_ACCEPTS_CALL_INFO[key]
def _enrich_http_exception_with_guardrail_context(exc: BaseException, callback: Any) -> None:
def _enrich_http_exception_with_guardrail_context(exc: BaseException, callback: CustomLogger) -> None:
"""
If `exc` is an HTTPException with a dict `detail`, mutate it in place to
add `guardrail_name` and `guardrail_mode` taken from the callback instance.
@ -394,11 +394,11 @@ class _CallbackCapabilities:
# Tuple[(resolved_callback, "override" | "apply_guardrail"), ...]
# Ordered the same as ``litellm.callbacks``; used to build the streaming
# iterator chain without re-scanning per request.
iterator_overrides: Tuple[Tuple[Any, str], ...] = field(default_factory=tuple)
iterator_overrides: tuple[tuple[CustomLogger, str], ...] = field(default_factory=tuple)
# Resolved CustomLogger callbacks in original order. Pre-resolving once
# avoids the per-request ``get_custom_logger_compatible_class`` walk for
# every string entry in ``litellm.callbacks``.
resolved_callbacks: Tuple[Any, ...] = field(default_factory=tuple)
resolved_callbacks: tuple[CustomLogger, ...] = field(default_factory=tuple)
class ProxyLogging:
@ -676,7 +676,7 @@ class ProxyLogging:
return synthetic_data
def _convert_llm_result_to_mcp_response(self, llm_result, request_obj) -> Optional[Any]:
def _convert_llm_result_to_mcp_response(self, llm_result, request_obj) -> MCPPreCallResponseObject | None:
"""
Convert LLM guardrail result back to MCP response format.
"""
@ -802,7 +802,9 @@ class ProxyLogging:
verbose_proxy_logger.error(f"Error in manual argument parsing: {e}")
return None
def _convert_llm_result_to_mcp_during_response(self, llm_result, request_obj) -> Optional[Any]:
def _convert_llm_result_to_mcp_during_response(
self, llm_result, request_obj
) -> MCPDuringCallResponseObject | None:
"""
Convert LLM guardrail result back to MCP during call response format.
"""
@ -1142,8 +1144,8 @@ class ProxyLogging:
self,
data: dict,
litellm_logging_obj: Any,
prompt_id: Any,
prompt_version: Any,
prompt_id: str,
prompt_version: int | None,
call_type: CallTypesLiteral,
) -> None:
"""Process prompt template if applicable."""
@ -1438,9 +1440,7 @@ class ProxyLogging:
data = result
elif (
_callback is not None
and isinstance(_callback, CustomLogger)
and "async_pre_call_hook" in vars(_callback.__class__)
"async_pre_call_hook" in vars(_callback.__class__)
and _callback.__class__.async_pre_call_hook != CustomLogger.async_pre_call_hook
):
if call_type == "call_mcp_tool" and user_api_key_dict is None:
@ -1614,7 +1614,7 @@ class ProxyLogging:
break
@staticmethod
async def _run_guardrail_with_metrics(callback: Any, coro: Awaitable[Any], hook_type: str) -> Any:
async def _run_guardrail_with_metrics(callback: CustomLogger, coro: Awaitable[Any], hook_type: str) -> Any:
"""
Await `coro`, recording its latency and status to the
`litellm_guardrail_latency_seconds` metric under `hook_type`, and
@ -1646,7 +1646,7 @@ class ProxyLogging:
@staticmethod
async def _wrap_streaming_iterator_with_enrichment(
callback: Any, gen: AsyncGenerator[Any, None]
callback: CustomLogger, gen: AsyncGenerator[Any, None]
) -> AsyncGenerator[Any, None]:
"""
Yield from `gen`; if iteration raises an HTTPException with dict detail,
@ -1691,8 +1691,11 @@ class ProxyLogging:
has_streaming_chunk_override = False
has_guardrail = False
has_pre_call_override = False
iterator_overrides: List[Tuple[Any, str]] = [] # (callback, kind)
resolved_callbacks: List[Any] = []
# (callback, kind) pairs; built alongside the 4 flags above and the
# resolved_callbacks list below in one pass over callbacks, since a
# functional per-field rewrite would re-walk this perf-sensitive scan.
iterator_overrides: list[tuple[CustomLogger, str]] = [] # mutable-ok: see comment above
resolved_callbacks: list[CustomLogger] = [] # mutable-ok: see comment above
for callback in callbacks:
if isinstance(callback, str):
@ -2795,7 +2798,7 @@ _DEPRECATED_KEY_CACHE_TTL_SECONDS = 60
async def _lookup_deprecated_key(
db: Any,
db: Union[PrismaWrapper, RoutingPrismaWrapper],
hashed_token: str,
) -> Optional[str]:
"""
@ -2875,7 +2878,7 @@ def _unpack_config_row(cached: Any) -> Optional[_ConfigRow]:
return None
async def get_config_param(prisma_client: Any, param_name: str) -> Optional[Any]:
async def get_config_param(prisma_client: "PrismaClient", param_name: str) -> Any | None:
"""Cached read of a LiteLLM_Config row; returns row, _ConfigRow shim, or None."""
cache_key = _config_cache_key(param_name)
cached = await litellm_config_cache.async_get_cache(cache_key)
@ -2893,7 +2896,7 @@ async def invalidate_config_param(param_name: str) -> None:
await litellm_config_cache.async_delete_cache(_config_cache_key(param_name))
async def prefetch_config_params(prisma_client: Any, param_names: List[str]) -> None:
async def prefetch_config_params(prisma_client: "PrismaClient", param_names: Sequence[str]) -> None:
"""Batch-load LiteLLM_Config rows into the cache with one find_many."""
if not param_names:
return
@ -5932,7 +5935,10 @@ def _check_and_merge_model_level_guardrails(
return _merge_guardrails_with_existing(data, model_level_guardrails)
def _merge_guardrails_with_existing(data: dict, model_level_guardrails: Any) -> dict:
def _merge_guardrails_with_existing(
data: dict, # mutable-ok: request payload dict, needs .copy()/.setdefault()
model_level_guardrails: Sequence[str] | str | None,
) -> dict: # mutable-ok: returns a merged copy of the request dict, not a read-only view
"""
Merge model-level guardrails with any existing guardrails in the request data.
@ -5952,11 +5958,13 @@ def _merge_guardrails_with_existing(data: dict, model_level_guardrails: Any) ->
existing_guardrails = [existing_guardrails] if existing_guardrails else []
# Ensure model_level_guardrails is a list
if not isinstance(model_level_guardrails, list):
model_level_guardrails = [model_level_guardrails] if model_level_guardrails else []
if isinstance(model_level_guardrails, list):
normalized_model_level_guardrails = model_level_guardrails
else:
normalized_model_level_guardrails = [model_level_guardrails] if model_level_guardrails else []
# Combine existing and model-level guardrails
metadata["guardrails"] = list(set(existing_guardrails + model_level_guardrails))
metadata["guardrails"] = list(set(existing_guardrails + normalized_model_level_guardrails))
return modified_data

View file

@ -1,6 +1,6 @@
{
"ANN001": {
"limit": 3118
"limit": 3116
},
"ANN002": {
"limit": 69
@ -24,7 +24,7 @@
"limit": 130
},
"ANN401": {
"limit": 2013
"limit": 1915
},
"ASYNC230": {
"limit": 14
@ -222,7 +222,7 @@
"limit": 38
},
"RET504": {
"limit": 716
"limit": 710
},
"RUF010": {
"limit": 874
@ -324,7 +324,7 @@
"limit": 883
},
"UP006": {
"limit": 12146
"limit": 12137
},
"UP007": {
"limit": 2526
@ -363,6 +363,6 @@
"limit": 105
},
"UP045": {
"limit": 17806
"limit": 17803
}
}

View file

@ -1,6 +1,6 @@
{
"LIT001": {
"limit": 23267
"limit": 23256
},
"LIT002": {
"limit": 27434
@ -24,6 +24,6 @@
"limit": 1004
},
"LIT009": {
"limit": 2474
"limit": 2472
}
}