mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
Merge pull request #38205 from BerriAI/litellm_decrease_anys_opus5_round2
refactor(repositories): type prisma table access with one generic protocol
This commit is contained in:
commit
99e1eaaca0
103 changed files with 2327 additions and 1332 deletions
|
|
@ -1,18 +1,18 @@
|
||||||
{
|
{
|
||||||
"reportAny": {
|
"reportAny": {
|
||||||
"limit": 19949
|
"limit": 18505
|
||||||
},
|
},
|
||||||
"reportArgumentType": {
|
"reportArgumentType": {
|
||||||
"limit": 2566
|
"limit": 2564
|
||||||
},
|
},
|
||||||
"reportAssignmentType": {
|
"reportAssignmentType": {
|
||||||
"limit": 320
|
"limit": 320
|
||||||
},
|
},
|
||||||
"reportAttributeAccessIssue": {
|
"reportAttributeAccessIssue": {
|
||||||
"limit": 488
|
"limit": 483
|
||||||
},
|
},
|
||||||
"reportCallIssue": {
|
"reportCallIssue": {
|
||||||
"limit": 114
|
"limit": 113
|
||||||
},
|
},
|
||||||
"reportConstantRedefinition": {
|
"reportConstantRedefinition": {
|
||||||
"limit": 40
|
"limit": 40
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
"limit": 19
|
"limit": 19
|
||||||
},
|
},
|
||||||
"reportExplicitAny": {
|
"reportExplicitAny": {
|
||||||
"limit": 6049
|
"limit": 5976
|
||||||
},
|
},
|
||||||
"reportFunctionMemberAccess": {
|
"reportFunctionMemberAccess": {
|
||||||
"limit": 7
|
"limit": 7
|
||||||
|
|
@ -45,7 +45,7 @@
|
||||||
"limit": 35
|
"limit": 35
|
||||||
},
|
},
|
||||||
"reportInvalidTypeForm": {
|
"reportInvalidTypeForm": {
|
||||||
"limit": 35
|
"limit": 34
|
||||||
},
|
},
|
||||||
"reportInvalidTypeVarUse": {
|
"reportInvalidTypeVarUse": {
|
||||||
"limit": 2
|
"limit": 2
|
||||||
|
|
@ -54,10 +54,10 @@
|
||||||
"limit": 0
|
"limit": 0
|
||||||
},
|
},
|
||||||
"reportMissingParameterType": {
|
"reportMissingParameterType": {
|
||||||
"limit": 5661
|
"limit": 5659
|
||||||
},
|
},
|
||||||
"reportMissingTypeArgument": {
|
"reportMissingTypeArgument": {
|
||||||
"limit": 15555
|
"limit": 15504
|
||||||
},
|
},
|
||||||
"reportMissingTypeStubs": {
|
"reportMissingTypeStubs": {
|
||||||
"limit": 40
|
"limit": 40
|
||||||
|
|
@ -72,7 +72,7 @@
|
||||||
"limit": 0
|
"limit": 0
|
||||||
},
|
},
|
||||||
"reportOptionalMemberAccess": {
|
"reportOptionalMemberAccess": {
|
||||||
"limit": 1061
|
"limit": 1058
|
||||||
},
|
},
|
||||||
"reportOptionalOperand": {
|
"reportOptionalOperand": {
|
||||||
"limit": 0
|
"limit": 0
|
||||||
|
|
@ -99,31 +99,31 @@
|
||||||
"limit": 0
|
"limit": 0
|
||||||
},
|
},
|
||||||
"reportUnknownArgumentType": {
|
"reportUnknownArgumentType": {
|
||||||
"limit": 44655
|
"limit": 44530
|
||||||
},
|
},
|
||||||
"reportUnknownLambdaType": {
|
"reportUnknownLambdaType": {
|
||||||
"limit": 109
|
"limit": 109
|
||||||
},
|
},
|
||||||
"reportUnknownMemberType": {
|
"reportUnknownMemberType": {
|
||||||
"limit": 39009
|
"limit": 38828
|
||||||
},
|
},
|
||||||
"reportUnknownParameterType": {
|
"reportUnknownParameterType": {
|
||||||
"limit": 19883
|
"limit": 19847
|
||||||
},
|
},
|
||||||
"reportUnknownVariableType": {
|
"reportUnknownVariableType": {
|
||||||
"limit": 30569
|
"limit": 30386
|
||||||
},
|
},
|
||||||
"reportUnnecessaryCast": {
|
"reportUnnecessaryCast": {
|
||||||
"limit": 117
|
"limit": 117
|
||||||
},
|
},
|
||||||
"reportUnnecessaryComparison": {
|
"reportUnnecessaryComparison": {
|
||||||
"limit": 699
|
"limit": 697
|
||||||
},
|
},
|
||||||
"reportUnnecessaryContains": {
|
"reportUnnecessaryContains": {
|
||||||
"limit": 5
|
"limit": 5
|
||||||
},
|
},
|
||||||
"reportUnnecessaryIsInstance": {
|
"reportUnnecessaryIsInstance": {
|
||||||
"limit": 836
|
"limit": 833
|
||||||
},
|
},
|
||||||
"reportUntypedBaseClass": {
|
"reportUntypedBaseClass": {
|
||||||
"limit": 0
|
"limit": 0
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ from litellm.constants import (
|
||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from prisma import models as prisma_models
|
||||||
|
|
||||||
from litellm.integrations.prometheus import PrometheusLogger
|
from litellm.integrations.prometheus import PrometheusLogger
|
||||||
from litellm.proxy._types import LiteLLM_ManagedObjectTable
|
from litellm.proxy._types import LiteLLM_ManagedObjectTable
|
||||||
from litellm.proxy.utils import PrismaClient, ProxyLogging
|
from litellm.proxy.utils import PrismaClient, ProxyLogging
|
||||||
|
|
@ -351,7 +353,7 @@ class CheckBatchCost:
|
||||||
return isinstance(error, (NotFoundError, openai.NotFoundError)) and output_file_id in str(error)
|
return isinstance(error, (NotFoundError, openai.NotFoundError)) and output_file_id in str(error)
|
||||||
|
|
||||||
async def _finalize_unbilled_terminal_job(
|
async def _finalize_unbilled_terminal_job(
|
||||||
self, job: "LiteLLM_ManagedObjectTable", response: "LiteLLMBatch"
|
self, job: "prisma_models.LiteLLM_ManagedObjectTable", response: "LiteLLMBatch"
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Persist a terminal batch that has nothing billable, converting any raw
|
"""Persist a terminal batch that has nothing billable, converting any raw
|
||||||
provider file ids to managed ids, and take it out of the poll page."""
|
provider file ids to managed ids, and take it out of the poll page."""
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,10 @@ class _PaginatedPrismaTable(Protocol[_TableRowT]):
|
||||||
|
|
||||||
def _paginated_table(repository: BaseRepository[_TableRowT]) -> _PaginatedPrismaTable[_TableRowT]:
|
def _paginated_table(repository: BaseRepository[_TableRowT]) -> _PaginatedPrismaTable[_TableRowT]:
|
||||||
"""View a repository's prisma table through the pagination surface budget metrics need."""
|
"""View a repository's prisma table through the pagination surface budget metrics need."""
|
||||||
return repository.table
|
return cast(
|
||||||
|
_PaginatedPrismaTable[_TableRowT],
|
||||||
|
repository.table, # cast-ok: prisma rows carry the budget columns the domain model declares
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class _OrgBudgetRow(Protocol):
|
class _OrgBudgetRow(Protocol):
|
||||||
|
|
|
||||||
|
|
@ -64,8 +64,8 @@ class TeamBase(LiteLLMPydanticObjectBase):
|
||||||
team_alias: str | None = None
|
team_alias: str | None = None
|
||||||
team_id: str | None = None
|
team_id: str | None = None
|
||||||
organization_id: str | None = None
|
organization_id: str | None = None
|
||||||
admins: list = []
|
admins: list[str] = []
|
||||||
members: list = []
|
members: list[str] = []
|
||||||
members_with_roles: list[Member] = []
|
members_with_roles: list[Member] = []
|
||||||
team_member_permissions: list[str] | None = None
|
team_member_permissions: list[str] | None = None
|
||||||
metadata: dict | None = None
|
metadata: dict | None = None
|
||||||
|
|
@ -75,7 +75,7 @@ class TeamBase(LiteLLMPydanticObjectBase):
|
||||||
soft_budget: float | None = None
|
soft_budget: float | None = None
|
||||||
budget_duration: str | None = None
|
budget_duration: str | None = None
|
||||||
budget_limits: list[BudgetLimitEntry] | None = None
|
budget_limits: list[BudgetLimitEntry] | None = None
|
||||||
models: list = []
|
models: list[str] = []
|
||||||
blocked: bool = False
|
blocked: bool = False
|
||||||
router_settings: dict | None = None
|
router_settings: dict | None = None
|
||||||
access_group_ids: list[str] | None = None
|
access_group_ids: list[str] | None = None
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import hashlib
|
||||||
import json
|
import json
|
||||||
from collections.abc import Awaitable, Callable, Iterable, Mapping, Sequence
|
from collections.abc import Awaitable, Callable, Iterable, Mapping, Sequence
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import TYPE_CHECKING, Any, Final, Protocol, TypedDict, TypeVar, cast
|
from typing import TYPE_CHECKING, Any, Final, Protocol, TypedDict, cast
|
||||||
|
|
||||||
from litellm._logging import verbose_proxy_logger
|
from litellm._logging import verbose_proxy_logger
|
||||||
from litellm._uuid import uuid
|
from litellm._uuid import uuid
|
||||||
|
|
@ -13,7 +13,6 @@ from litellm.llms.custom_httpx.http_handler import get_async_httpx_client
|
||||||
from litellm.proxy._experimental.mcp_server.oauth_utils import build_upstream_oauth2_token_request
|
from litellm.proxy._experimental.mcp_server.oauth_utils import build_upstream_oauth2_token_request
|
||||||
from litellm.proxy._types import (
|
from litellm.proxy._types import (
|
||||||
LiteLLM_MCPServerTable,
|
LiteLLM_MCPServerTable,
|
||||||
LiteLLM_ObjectPermissionTable,
|
|
||||||
MCPApprovalStatus,
|
MCPApprovalStatus,
|
||||||
MCPEnvVar,
|
MCPEnvVar,
|
||||||
MCPEnvVarScope,
|
MCPEnvVarScope,
|
||||||
|
|
@ -30,6 +29,7 @@ from litellm.proxy.common_utils.encrypt_decrypt_utils import (
|
||||||
)
|
)
|
||||||
from litellm.proxy.utils import PrismaClient
|
from litellm.proxy.utils import PrismaClient
|
||||||
from litellm.repositories.object_permission_repository import ObjectPermissionRepository
|
from litellm.repositories.object_permission_repository import ObjectPermissionRepository
|
||||||
|
from litellm.repositories.prisma_protocols import TableActions
|
||||||
from litellm.repositories.table_repositories import (
|
from litellm.repositories.table_repositories import (
|
||||||
MCPServerOAuthClientRepository,
|
MCPServerOAuthClientRepository,
|
||||||
MCPServerRepository,
|
MCPServerRepository,
|
||||||
|
|
@ -48,34 +48,9 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
from litellm.types.mcp_server.mcp_server_manager import MCPServer
|
from litellm.types.mcp_server.mcp_server_manager import MCPServer
|
||||||
|
|
||||||
_RowT = TypeVar("_RowT")
|
|
||||||
|
|
||||||
|
|
||||||
class _TableActions(Protocol[_RowT]):
|
|
||||||
async def find_unique(
|
|
||||||
self, where: Mapping[str, object], include: Mapping[str, object] | None = None
|
|
||||||
) -> _RowT | None: ...
|
|
||||||
|
|
||||||
async def find_many(
|
|
||||||
self,
|
|
||||||
take: int | None = None,
|
|
||||||
where: Mapping[str, object] | None = None,
|
|
||||||
order: Mapping[str, object] | None = None,
|
|
||||||
) -> list[_RowT]: ...
|
|
||||||
|
|
||||||
async def create(self, data: Mapping[str, object]) -> _RowT: ...
|
|
||||||
|
|
||||||
async def upsert(self, where: Mapping[str, object], data: Mapping[str, object]) -> _RowT: ...
|
|
||||||
|
|
||||||
async def update(self, where: Mapping[str, object], data: Mapping[str, object]) -> _RowT | None: ...
|
|
||||||
|
|
||||||
async def delete(self, where: Mapping[str, object]) -> _RowT | None: ...
|
|
||||||
|
|
||||||
async def delete_many(self, where: Mapping[str, object] | None = None) -> int: ...
|
|
||||||
|
|
||||||
|
|
||||||
class _UserEnvVarsTransactionClient(Protocol):
|
class _UserEnvVarsTransactionClient(Protocol):
|
||||||
litellm_mcpuserenvvars: "_TableActions[prisma_db_models.LiteLLM_MCPUserEnvVars]"
|
litellm_mcpuserenvvars: "TableActions[prisma_db_models.LiteLLM_MCPUserEnvVars]"
|
||||||
|
|
||||||
async def execute_raw(self, query: str, *args: object) -> int: ...
|
async def execute_raw(self, query: str, *args: object) -> int: ...
|
||||||
|
|
||||||
|
|
@ -473,15 +448,15 @@ def _credentials_blob_to_mutable_dict(blob: str | Mapping[str, object]) -> dict[
|
||||||
|
|
||||||
def _mcp_server_table_actions(
|
def _mcp_server_table_actions(
|
||||||
prisma_client: PrismaClient,
|
prisma_client: PrismaClient,
|
||||||
) -> "_TableActions[prisma_db_models.LiteLLM_MCPServerTable]":
|
) -> "TableActions[prisma_db_models.LiteLLM_MCPServerTable]":
|
||||||
table: Final[_TableActions[prisma_db_models.LiteLLM_MCPServerTable]] = MCPServerRepository(prisma_client).table
|
table: Final[TableActions[prisma_db_models.LiteLLM_MCPServerTable]] = MCPServerRepository(prisma_client).table
|
||||||
return table
|
return table
|
||||||
|
|
||||||
|
|
||||||
def _verification_token_table_actions(
|
def _verification_token_table_actions(
|
||||||
prisma_client: PrismaClient,
|
prisma_client: PrismaClient,
|
||||||
) -> "_TableActions[prisma_db_models.LiteLLM_VerificationToken]":
|
) -> "TableActions[prisma_db_models.LiteLLM_VerificationToken]":
|
||||||
table: Final[_TableActions[prisma_db_models.LiteLLM_VerificationToken]] = VerificationTokenRepository(
|
table: Final[TableActions[prisma_db_models.LiteLLM_VerificationToken]] = VerificationTokenRepository(
|
||||||
prisma_client
|
prisma_client
|
||||||
).table
|
).table
|
||||||
return table
|
return table
|
||||||
|
|
@ -489,15 +464,15 @@ def _verification_token_table_actions(
|
||||||
|
|
||||||
def _team_table_actions(
|
def _team_table_actions(
|
||||||
prisma_client: PrismaClient,
|
prisma_client: PrismaClient,
|
||||||
) -> "_TableActions[prisma_db_models.LiteLLM_TeamTable]":
|
) -> "TableActions[prisma_db_models.LiteLLM_TeamTable]":
|
||||||
table: Final[_TableActions[prisma_db_models.LiteLLM_TeamTable]] = TeamRepository(prisma_client).table
|
table: Final[TableActions[prisma_db_models.LiteLLM_TeamTable]] = TeamRepository(prisma_client).table
|
||||||
return table
|
return table
|
||||||
|
|
||||||
|
|
||||||
def _oauth_client_table_actions(
|
def _oauth_client_table_actions(
|
||||||
prisma_client: PrismaClient,
|
prisma_client: PrismaClient,
|
||||||
) -> "_TableActions[prisma_db_models.LiteLLM_MCPServerOAuthClient]":
|
) -> "TableActions[prisma_db_models.LiteLLM_MCPServerOAuthClient]":
|
||||||
table: Final[_TableActions[prisma_db_models.LiteLLM_MCPServerOAuthClient]] = MCPServerOAuthClientRepository(
|
table: Final[TableActions[prisma_db_models.LiteLLM_MCPServerOAuthClient]] = MCPServerOAuthClientRepository(
|
||||||
prisma_client
|
prisma_client
|
||||||
).table
|
).table
|
||||||
return table
|
return table
|
||||||
|
|
@ -511,7 +486,7 @@ def _db_transaction_manager(prisma_client: PrismaClient) -> _UserEnvVarsTransact
|
||||||
async def _db_find_mcp_server_rows(
|
async def _db_find_mcp_server_rows(
|
||||||
prisma_client: PrismaClient,
|
prisma_client: PrismaClient,
|
||||||
where: "prisma_db_types.LiteLLM_MCPServerTableWhereInput | None" = None,
|
where: "prisma_db_types.LiteLLM_MCPServerTableWhereInput | None" = None,
|
||||||
) -> "list[prisma_db_models.LiteLLM_MCPServerTable]":
|
) -> "Sequence[prisma_db_models.LiteLLM_MCPServerTable]":
|
||||||
return await _mcp_server_table_actions(prisma_client).find_many(where=where)
|
return await _mcp_server_table_actions(prisma_client).find_many(where=where)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -526,17 +501,19 @@ async def _db_update_mcp_server_row(
|
||||||
server_id: str,
|
server_id: str,
|
||||||
data: "prisma_db_types.LiteLLM_MCPServerTableUpdateInput",
|
data: "prisma_db_types.LiteLLM_MCPServerTableUpdateInput",
|
||||||
) -> "prisma_db_models.LiteLLM_MCPServerTable":
|
) -> "prisma_db_models.LiteLLM_MCPServerTable":
|
||||||
row: Final[prisma_db_models.LiteLLM_MCPServerTable] = await MCPServerRepository(prisma_client).table.update(
|
row: Final[prisma_db_models.LiteLLM_MCPServerTable | None] = await _mcp_server_table_actions(prisma_client).update(
|
||||||
where={"server_id": server_id},
|
where={"server_id": server_id},
|
||||||
data=data,
|
data=data,
|
||||||
)
|
)
|
||||||
|
if row is None:
|
||||||
|
raise ValueError(f"MCP server not found, passed server_id={server_id}")
|
||||||
return row
|
return row
|
||||||
|
|
||||||
|
|
||||||
def _user_credential_actions(
|
def _user_credential_actions(
|
||||||
prisma_client: PrismaClient,
|
prisma_client: PrismaClient,
|
||||||
) -> "_TableActions[prisma_db_models.LiteLLM_MCPUserCredentials]":
|
) -> "TableActions[prisma_db_models.LiteLLM_MCPUserCredentials]":
|
||||||
table: Final[_TableActions[prisma_db_models.LiteLLM_MCPUserCredentials]] = MCPUserCredentialsRepository(
|
table: Final[TableActions[prisma_db_models.LiteLLM_MCPUserCredentials]] = MCPUserCredentialsRepository(
|
||||||
prisma_client
|
prisma_client
|
||||||
).table
|
).table
|
||||||
return table
|
return table
|
||||||
|
|
@ -544,8 +521,8 @@ def _user_credential_actions(
|
||||||
|
|
||||||
def _user_env_var_actions(
|
def _user_env_var_actions(
|
||||||
prisma_client: PrismaClient,
|
prisma_client: PrismaClient,
|
||||||
) -> "_TableActions[prisma_db_models.LiteLLM_MCPUserEnvVars]":
|
) -> "TableActions[prisma_db_models.LiteLLM_MCPUserEnvVars]":
|
||||||
table: Final[_TableActions[prisma_db_models.LiteLLM_MCPUserEnvVars]] = prisma_client.db.litellm_mcpuserenvvars
|
table: Final[TableActions[prisma_db_models.LiteLLM_MCPUserEnvVars]] = prisma_client.db.litellm_mcpuserenvvars
|
||||||
return table
|
return table
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -560,7 +537,7 @@ async def _db_find_user_credential_row(
|
||||||
async def _db_find_user_credential_rows(
|
async def _db_find_user_credential_rows(
|
||||||
prisma_client: PrismaClient,
|
prisma_client: PrismaClient,
|
||||||
where: "prisma_db_types.LiteLLM_MCPUserCredentialsWhereInput | None" = None,
|
where: "prisma_db_types.LiteLLM_MCPUserCredentialsWhereInput | None" = None,
|
||||||
) -> "list[prisma_db_models.LiteLLM_MCPUserCredentials]":
|
) -> "Sequence[prisma_db_models.LiteLLM_MCPUserCredentials]":
|
||||||
return await _user_credential_actions(prisma_client).find_many(where=where)
|
return await _user_credential_actions(prisma_client).find_many(where=where)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -583,7 +560,7 @@ async def _db_upsert_user_credential_row(
|
||||||
async def _db_find_user_env_var_rows(
|
async def _db_find_user_env_var_rows(
|
||||||
prisma_client: PrismaClient,
|
prisma_client: PrismaClient,
|
||||||
where: "prisma_db_types.LiteLLM_MCPUserEnvVarsWhereInput | None" = None,
|
where: "prisma_db_types.LiteLLM_MCPUserEnvVarsWhereInput | None" = None,
|
||||||
) -> "list[prisma_db_models.LiteLLM_MCPUserEnvVars]":
|
) -> "Sequence[prisma_db_models.LiteLLM_MCPUserEnvVars]":
|
||||||
return await _user_env_var_actions(prisma_client).find_many(where=where)
|
return await _user_env_var_actions(prisma_client).find_many(where=where)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -658,7 +635,7 @@ async def get_mcp_servers(prisma_client: PrismaClient, server_ids: Iterable[str]
|
||||||
"""
|
"""
|
||||||
Returns the matching mcp servers from the db with the server_ids
|
Returns the matching mcp servers from the db with the server_ids
|
||||||
"""
|
"""
|
||||||
_mcp_servers: Final[list[prisma_db_models.LiteLLM_MCPServerTable]] = await _mcp_server_table_actions(
|
_mcp_servers: Final[Sequence[prisma_db_models.LiteLLM_MCPServerTable]] = await _mcp_server_table_actions(
|
||||||
prisma_client
|
prisma_client
|
||||||
).find_many(
|
).find_many(
|
||||||
where={
|
where={
|
||||||
|
|
@ -745,13 +722,13 @@ async def get_all_mcp_servers_for_user(
|
||||||
|
|
||||||
async def get_objectpermissions_for_mcp_server(
|
async def get_objectpermissions_for_mcp_server(
|
||||||
prisma_client: PrismaClient, mcp_server_id: str
|
prisma_client: PrismaClient, mcp_server_id: str
|
||||||
) -> list[LiteLLM_ObjectPermissionTable]:
|
) -> "Sequence[prisma_db_models.LiteLLM_ObjectPermissionTable]":
|
||||||
"""
|
"""
|
||||||
Get all the object permissions records and the associated team and verficiationtoken records that have access to the mcp server
|
Get all the object permissions records and the associated team and verficiationtoken records that have access to the mcp server
|
||||||
"""
|
"""
|
||||||
object_permission_records: Final[list[LiteLLM_ObjectPermissionTable]] = await ObjectPermissionRepository(
|
object_permission_records: Final[
|
||||||
prisma_client
|
Sequence[prisma_db_models.LiteLLM_ObjectPermissionTable]
|
||||||
).table.find_many(
|
] = await ObjectPermissionRepository(prisma_client).table.find_many(
|
||||||
where={
|
where={
|
||||||
"mcp_servers": {"has": mcp_server_id},
|
"mcp_servers": {"has": mcp_server_id},
|
||||||
},
|
},
|
||||||
|
|
@ -766,19 +743,19 @@ async def get_objectpermissions_for_mcp_server(
|
||||||
|
|
||||||
async def get_virtualkeys_for_mcp_server(
|
async def get_virtualkeys_for_mcp_server(
|
||||||
prisma_client: PrismaClient, server_id: str
|
prisma_client: PrismaClient, server_id: str
|
||||||
) -> "list[prisma_db_models.LiteLLM_VerificationToken]":
|
) -> "Sequence[prisma_db_models.LiteLLM_VerificationToken]":
|
||||||
"""
|
"""
|
||||||
Get all the virtual keys that have access to the mcp server
|
Get all the virtual keys that have access to the mcp server
|
||||||
"""
|
"""
|
||||||
virtual_keys: Final[list[prisma_db_models.LiteLLM_VerificationToken] | None] = await VerificationTokenRepository(
|
virtual_keys: Final[
|
||||||
prisma_client
|
Sequence[prisma_db_models.LiteLLM_VerificationToken] | None
|
||||||
).table.find_many(
|
] = await VerificationTokenRepository(prisma_client).table.find_many(
|
||||||
where={
|
where={
|
||||||
"mcp_servers": {"has": server_id},
|
"mcp_servers": {"has": server_id},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
if virtual_keys is None:
|
if virtual_keys is None: # pyright: ignore[reportUnnecessaryComparison] # unreachable per seam types; kept as-is
|
||||||
return []
|
return []
|
||||||
return virtual_keys
|
return virtual_keys
|
||||||
|
|
||||||
|
|
@ -860,7 +837,7 @@ async def delete_mcp_server(
|
||||||
invalidate_token_cache = global_mcp_server_manager.invalidate_user_oauth_token_cache
|
invalidate_token_cache = global_mcp_server_manager.invalidate_user_oauth_token_cache
|
||||||
for user_id in credential_user_ids:
|
for user_id in credential_user_ids:
|
||||||
await invalidate_token_cache(user_id, server_id)
|
await invalidate_token_cache(user_id, server_id)
|
||||||
return deleted_server
|
return deleted_server # pyright: ignore[reportReturnType] # prisma row, not domain LiteLLM_MCPServerTable
|
||||||
|
|
||||||
|
|
||||||
async def create_mcp_server(
|
async def create_mcp_server(
|
||||||
|
|
@ -880,7 +857,7 @@ async def create_mcp_server(
|
||||||
data_dict["updated_by"] = touched_by
|
data_dict["updated_by"] = touched_by
|
||||||
|
|
||||||
new_mcp_server: Final[LiteLLM_MCPServerTable] = await MCPServerRepository(prisma_client).table.create(
|
new_mcp_server: Final[LiteLLM_MCPServerTable] = await MCPServerRepository(prisma_client).table.create(
|
||||||
data=data_dict
|
data=data_dict, # pyright: ignore[reportAssignmentType] # prisma row, not domain LiteLLM_MCPServerTable
|
||||||
)
|
)
|
||||||
|
|
||||||
_decrypt_env_vars_on_returned_row(new_mcp_server)
|
_decrypt_env_vars_on_returned_row(new_mcp_server)
|
||||||
|
|
@ -982,7 +959,7 @@ async def update_mcp_server(
|
||||||
data: UpdateMCPServerRequest,
|
data: UpdateMCPServerRequest,
|
||||||
touched_by: str,
|
touched_by: str,
|
||||||
fields_set: set[str] | None = None,
|
fields_set: set[str] | None = None,
|
||||||
) -> LiteLLM_MCPServerTable:
|
) -> LiteLLM_MCPServerTable | None:
|
||||||
"""
|
"""
|
||||||
Update a new mcp server record in the db
|
Update a new mcp server record in the db
|
||||||
"""
|
"""
|
||||||
|
|
@ -1093,9 +1070,9 @@ async def update_mcp_server(
|
||||||
|
|
||||||
data_dict["credentials"] = Json(None)
|
data_dict["credentials"] = Json(None)
|
||||||
|
|
||||||
updated_mcp_server: Final[LiteLLM_MCPServerTable] = await MCPServerRepository(prisma_client).table.update(
|
updated_mcp_server: Final[LiteLLM_MCPServerTable | None] = await MCPServerRepository(prisma_client).table.update(
|
||||||
where={"server_id": data.server_id},
|
where={"server_id": data.server_id},
|
||||||
data=data_dict,
|
data=data_dict, # pyright: ignore[reportAssignmentType] # prisma row, not domain LiteLLM_MCPServerTable
|
||||||
)
|
)
|
||||||
|
|
||||||
_decrypt_env_vars_on_returned_row(updated_mcp_server)
|
_decrypt_env_vars_on_returned_row(updated_mcp_server)
|
||||||
|
|
@ -1181,7 +1158,7 @@ async def rotate_mcp_server_credentials_master_key(prisma_client: PrismaClient,
|
||||||
)
|
)
|
||||||
updated += 1
|
updated += 1
|
||||||
|
|
||||||
oauth_clients: Final[list[prisma_db_models.LiteLLM_MCPServerOAuthClient]] = await _oauth_client_table_actions(
|
oauth_clients: Final[Sequence[prisma_db_models.LiteLLM_MCPServerOAuthClient]] = await _oauth_client_table_actions(
|
||||||
prisma_client
|
prisma_client
|
||||||
).find_many()
|
).find_many()
|
||||||
oauth_updated = 0
|
oauth_updated = 0
|
||||||
|
|
@ -1914,7 +1891,7 @@ async def get_mcp_submissions(
|
||||||
along with a summary count breakdown by approval_status.
|
along with a summary count breakdown by approval_status.
|
||||||
Mirrors get_guardrail_submissions() from guardrail_endpoints.py.
|
Mirrors get_guardrail_submissions() from guardrail_endpoints.py.
|
||||||
"""
|
"""
|
||||||
rows: Final[list[prisma_db_models.LiteLLM_MCPServerTable]] = await _mcp_server_table_actions(
|
rows: Final[Sequence[prisma_db_models.LiteLLM_MCPServerTable]] = await _mcp_server_table_actions(
|
||||||
prisma_client
|
prisma_client
|
||||||
).find_many(
|
).find_many(
|
||||||
where={"submitted_at": {"not": None}},
|
where={"submitted_at": {"not": None}},
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import json
|
||||||
from collections.abc import Iterator, Mapping, Sequence
|
from collections.abc import Iterator, Mapping, Sequence
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from types import MappingProxyType
|
from types import MappingProxyType
|
||||||
from typing import Any, Final, NamedTuple, Protocol, TypedDict
|
from typing import TYPE_CHECKING, Any, Final, NamedTuple, Protocol, TypedDict
|
||||||
|
|
||||||
import litellm
|
import litellm
|
||||||
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
|
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
|
||||||
|
|
@ -12,9 +12,13 @@ from litellm.proxy.management_helpers.object_permission_utils import (
|
||||||
handle_update_object_permission_common,
|
handle_update_object_permission_common,
|
||||||
)
|
)
|
||||||
from litellm.proxy.utils import PrismaClient
|
from litellm.proxy.utils import PrismaClient
|
||||||
|
from litellm.repositories.prisma_protocols import TableActions
|
||||||
from litellm.repositories.table_repositories import AgentsRepository, ObjectPermissionRepository
|
from litellm.repositories.table_repositories import AgentsRepository, ObjectPermissionRepository
|
||||||
from litellm.types.agents import AgentConfig, AgentResponse, PatchAgentRequest
|
from litellm.types.agents import AgentConfig, AgentResponse, PatchAgentRequest
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from prisma import models as prisma_models
|
||||||
|
|
||||||
|
|
||||||
class AgentObjectPermissionRecord(Protocol):
|
class AgentObjectPermissionRecord(Protocol):
|
||||||
def model_dump(self) -> dict[str, object]: ...
|
def model_dump(self) -> dict[str, object]: ...
|
||||||
|
|
@ -42,11 +46,20 @@ class AgentRecordDump(TypedDict):
|
||||||
|
|
||||||
|
|
||||||
class AgentRecord(Protocol):
|
class AgentRecord(Protocol):
|
||||||
agent_id: str
|
@property
|
||||||
agent_name: str
|
def agent_id(self) -> str: ...
|
||||||
object_permission_id: str | None
|
|
||||||
object_permission: AgentObjectPermissionRecord | None
|
@property
|
||||||
spend: float
|
def agent_name(self) -> str: ...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def object_permission_id(self) -> str | None: ...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def object_permission(self) -> AgentObjectPermissionRecord | None: ...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def spend(self) -> float: ...
|
||||||
|
|
||||||
def model_dump(self) -> AgentRecordDump: ...
|
def model_dump(self) -> AgentRecordDump: ...
|
||||||
|
|
||||||
|
|
@ -57,50 +70,47 @@ class AgentTableClient(Protocol):
|
||||||
async def create(
|
async def create(
|
||||||
self,
|
self,
|
||||||
data: Mapping[str, object],
|
data: Mapping[str, object],
|
||||||
include: Mapping[str, bool] | None = None,
|
include: Mapping[str, object] | None = None,
|
||||||
) -> AgentRecord: ...
|
) -> AgentRecord: ...
|
||||||
|
|
||||||
async def find_unique(
|
async def find_unique(
|
||||||
self,
|
self,
|
||||||
where: Mapping[str, object],
|
where: Mapping[str, object],
|
||||||
include: Mapping[str, bool] | None = None,
|
include: Mapping[str, object] | None = None,
|
||||||
) -> AgentRecord | None: ...
|
) -> AgentRecord | None: ...
|
||||||
|
|
||||||
async def find_many(
|
async def find_many(
|
||||||
self,
|
self,
|
||||||
where: Mapping[str, object] | None = None,
|
where: Mapping[str, object] | None = None,
|
||||||
order: Mapping[str, str] | None = None,
|
order: Mapping[str, str] | None = None,
|
||||||
include: Mapping[str, bool] | None = None,
|
include: Mapping[str, object] | None = None,
|
||||||
) -> Sequence[AgentRecord]: ...
|
) -> Sequence[AgentRecord]: ...
|
||||||
|
|
||||||
async def update(
|
async def update(
|
||||||
self,
|
self,
|
||||||
where: Mapping[str, object],
|
|
||||||
data: Mapping[str, object],
|
data: Mapping[str, object],
|
||||||
include: Mapping[str, bool] | None = None,
|
where: Mapping[str, object],
|
||||||
) -> AgentRecord: ...
|
include: Mapping[str, object] | None = None,
|
||||||
|
) -> AgentRecord | None: ...
|
||||||
|
|
||||||
async def delete(self, where: Mapping[str, object]) -> AgentRecord: ...
|
async def delete(
|
||||||
|
self,
|
||||||
|
where: Mapping[str, object],
|
||||||
|
include: Mapping[str, object] | None = None,
|
||||||
|
) -> AgentRecord | None: ...
|
||||||
|
|
||||||
|
|
||||||
def agents_table(prisma_client: PrismaClient) -> AgentTableClient:
|
def agents_table(prisma_client: PrismaClient) -> AgentTableClient:
|
||||||
table: Final[AgentTableClient] = AgentsRepository(prisma_client).table
|
table: Final[AgentTableClient] = AgentsRepository(prisma_client).table # pyright: ignore[reportAssignmentType] # prisma rows type model_dump() as dict[str, Any]
|
||||||
return table
|
return table
|
||||||
|
|
||||||
|
|
||||||
class ObjectPermissionGrantRecord(Protocol):
|
def object_permission_table(
|
||||||
object_permission_id: str
|
prisma_client: PrismaClient,
|
||||||
agents: list[str] | None
|
) -> "TableActions[prisma_models.LiteLLM_ObjectPermissionTable]":
|
||||||
|
table: Final[TableActions[prisma_models.LiteLLM_ObjectPermissionTable]] = ObjectPermissionRepository(
|
||||||
|
prisma_client
|
||||||
class ObjectPermissionTableClient(Protocol):
|
).table
|
||||||
async def find_many(self, where: Mapping[str, object]) -> Sequence[ObjectPermissionGrantRecord]: ...
|
|
||||||
|
|
||||||
async def update_many(self, where: Mapping[str, object], data: Mapping[str, object]) -> int: ...
|
|
||||||
|
|
||||||
|
|
||||||
def object_permission_table(prisma_client: PrismaClient) -> ObjectPermissionTableClient:
|
|
||||||
table: Final[ObjectPermissionTableClient] = ObjectPermissionRepository(prisma_client).table
|
|
||||||
return table
|
return table
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -222,7 +232,9 @@ class AgentRegistry:
|
||||||
self.load_agents_from_config(agent_config if agent_config is not None else self.config_agents)
|
self.load_agents_from_config(agent_config if agent_config is not None else self.config_agents)
|
||||||
return self.agent_list
|
return self.agent_list
|
||||||
|
|
||||||
async def migrate_legacy_grant_ids(self, table: ObjectPermissionTableClient) -> GrantMigrationResult:
|
async def migrate_legacy_grant_ids(
|
||||||
|
self, table: "TableActions[prisma_models.LiteLLM_ObjectPermissionTable]"
|
||||||
|
) -> GrantMigrationResult:
|
||||||
"""
|
"""
|
||||||
Rewrite object_permission.agents rows holding a legacy full-entry hash to the
|
Rewrite object_permission.agents rows holding a legacy full-entry hash to the
|
||||||
stable name-derived id.
|
stable name-derived id.
|
||||||
|
|
@ -360,6 +372,8 @@ class AgentRegistry:
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
deleted_agent: Final = await agents_table(prisma_client).delete(where={"agent_id": agent_id})
|
deleted_agent: Final = await agents_table(prisma_client).delete(where={"agent_id": agent_id})
|
||||||
|
if deleted_agent is None:
|
||||||
|
raise ValueError(f"Agent not found, passed agent_id={agent_id}")
|
||||||
return dict(deleted_agent)
|
return dict(deleted_agent)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception(f"Error deleting agent from DB: {e}")
|
raise Exception(f"Error deleting agent from DB: {e}")
|
||||||
|
|
@ -386,12 +400,12 @@ class AgentRegistry:
|
||||||
The patched agent
|
The patched agent
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
existing_agent = await AgentsRepository(prisma_client).table.find_unique(where={"agent_id": agent_id})
|
existing_row: Final = await AgentsRepository(prisma_client).table.find_unique(
|
||||||
if existing_agent is not None:
|
where={"agent_id": agent_id} # mutable-ok: prisma filters are plain dicts
|
||||||
existing_agent = dict(existing_agent)
|
)
|
||||||
|
if existing_row is None:
|
||||||
if existing_agent is None:
|
|
||||||
raise Exception(f"Agent with ID {agent_id} not found")
|
raise Exception(f"Agent with ID {agent_id} not found")
|
||||||
|
existing_agent: Final = dict(existing_row)
|
||||||
|
|
||||||
augment_agent: Final = {**existing_agent, **agent}
|
augment_agent: Final = {**existing_agent, **agent}
|
||||||
update_data: Final[dict[str, Any]] = {}
|
update_data: Final[dict[str, Any]] = {}
|
||||||
|
|
@ -436,6 +450,8 @@ class AgentRegistry:
|
||||||
},
|
},
|
||||||
include={"object_permission": True},
|
include={"object_permission": True},
|
||||||
)
|
)
|
||||||
|
if patched_agent is None:
|
||||||
|
raise ValueError(f"Agent not found, passed agent_id={agent_id}")
|
||||||
patched_agent_dict: Final = patched_agent.model_dump()
|
patched_agent_dict: Final = patched_agent.model_dump()
|
||||||
if patched_agent.object_permission is not None:
|
if patched_agent.object_permission is not None:
|
||||||
try:
|
try:
|
||||||
|
|
@ -523,6 +539,8 @@ class AgentRegistry:
|
||||||
include={"object_permission": True},
|
include={"object_permission": True},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if updated_agent is None:
|
||||||
|
raise ValueError(f"Agent not found, passed agent_id={agent_id}")
|
||||||
updated_agent_dict: Final = updated_agent.model_dump()
|
updated_agent_dict: Final = updated_agent.model_dump()
|
||||||
if updated_agent.object_permission is not None:
|
if updated_agent.object_permission is not None:
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -543,7 +543,7 @@ async def update_plugin(
|
||||||
|
|
||||||
manifest: Final[Mapping[str, object]] = _build_plugin_manifest(plugin_name, request)
|
manifest: Final[Mapping[str, object]] = _build_plugin_manifest(plugin_name, request)
|
||||||
|
|
||||||
plugin: Final[_PluginRecord] = await ClaudeCodePluginRepository(prisma_client).table.update(
|
plugin: Final[_PluginRecord | None] = await ClaudeCodePluginRepository(prisma_client).table.update(
|
||||||
where={"name": plugin_name}, # mutable-ok: prisma query arguments must be plain dicts
|
where={"name": plugin_name}, # mutable-ok: prisma query arguments must be plain dicts
|
||||||
data={ # mutable-ok: prisma query arguments must be plain dicts
|
data={ # mutable-ok: prisma query arguments must be plain dicts
|
||||||
"version": request.version,
|
"version": request.version,
|
||||||
|
|
@ -553,6 +553,8 @@ async def update_plugin(
|
||||||
"updated_at": datetime.now(timezone.utc),
|
"updated_at": datetime.now(timezone.utc),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
if plugin is None:
|
||||||
|
raise _error_response(404, f"Plugin '{plugin_name}' not found")
|
||||||
|
|
||||||
verbose_proxy_logger.info("Plugin %s updated successfully", plugin_name)
|
verbose_proxy_logger.info("Plugin %s updated successfully", plugin_name)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -157,7 +157,12 @@ class _PrismaVectorStoreRow(Protocol):
|
||||||
|
|
||||||
class _PrismaUserRow(Protocol):
|
class _PrismaUserRow(Protocol):
|
||||||
user_id: str
|
user_id: str
|
||||||
organization_memberships: Sequence[LiteLLM_OrganizationMembershipTable | None] | None
|
|
||||||
|
@property
|
||||||
|
def organization_memberships(self) -> Sequence[_PrismaModelDumpRow | None] | None: ...
|
||||||
|
|
||||||
|
@organization_memberships.setter
|
||||||
|
def organization_memberships(self, value: Sequence[_PrismaModelDumpRow] | None) -> None: ...
|
||||||
|
|
||||||
def __iter__(self) -> Iterator[tuple[str, object]]: ...
|
def __iter__(self) -> Iterator[tuple[str, object]]: ...
|
||||||
|
|
||||||
|
|
@ -215,9 +220,14 @@ def _user_table(repo: _PrismaTableHolder[_PrismaUserRow]) -> _PrismaAuthTable[_P
|
||||||
return repo.table
|
return repo.table
|
||||||
|
|
||||||
|
|
||||||
|
class _VectorStorePermissionsRow(Protocol):
|
||||||
|
@property
|
||||||
|
def vector_stores(self) -> Sequence[str] | None: ...
|
||||||
|
|
||||||
|
|
||||||
def _object_permission_table(
|
def _object_permission_table(
|
||||||
repo: _PrismaTableHolder[LiteLLM_ObjectPermissionTable],
|
repo: _PrismaTableHolder[_VectorStorePermissionsRow],
|
||||||
) -> _PrismaAuthTable[LiteLLM_ObjectPermissionTable]:
|
) -> _PrismaAuthTable[_VectorStorePermissionsRow]:
|
||||||
return repo.table
|
return repo.table
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -5376,7 +5386,7 @@ async def vector_store_access_check(
|
||||||
def _can_object_call_vector_stores(
|
def _can_object_call_vector_stores(
|
||||||
object_type: Literal["key", "team", "org"],
|
object_type: Literal["key", "team", "org"],
|
||||||
vector_store_ids_to_run: list[str],
|
vector_store_ids_to_run: list[str],
|
||||||
object_permissions: LiteLLM_ObjectPermissionTable | None,
|
object_permissions: _VectorStorePermissionsRow | None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Raises ProxyException if the object (key, team, org) cannot access the specific vector store.
|
Raises ProxyException if the object (key, team, org) cannot access the specific vector store.
|
||||||
|
|
|
||||||
|
|
@ -199,6 +199,15 @@ class _UserModelBudgetLimiter(Protocol):
|
||||||
) -> bool: ...
|
) -> bool: ...
|
||||||
|
|
||||||
|
|
||||||
|
class _TokenTeamModels(Protocol):
|
||||||
|
@property
|
||||||
|
def team_models(self) -> list[str]: ...
|
||||||
|
|
||||||
|
|
||||||
|
def _token_team_models(valid_token: _TokenTeamModels) -> list[str]:
|
||||||
|
return valid_token.team_models
|
||||||
|
|
||||||
|
|
||||||
async def _read_user_model_max_budget(
|
async def _read_user_model_max_budget(
|
||||||
user_id: str | None,
|
user_id: str | None,
|
||||||
prisma_client: PrismaClient | None,
|
prisma_client: PrismaClient | None,
|
||||||
|
|
@ -1992,7 +2001,7 @@ async def _user_api_key_auth_builder(
|
||||||
include={"litellm_budget_table": True},
|
include={"litellm_budget_table": True},
|
||||||
)
|
)
|
||||||
if _db_member is not None:
|
if _db_member is not None:
|
||||||
team_member_info = LiteLLM_TeamMembership(**_db_member.dict())
|
team_member_info = LiteLLM_TeamMembership(**_db_member.model_dump())
|
||||||
await user_api_key_cache.async_set_cache(
|
await user_api_key_cache.async_set_cache(
|
||||||
key=_cache_key,
|
key=_cache_key,
|
||||||
value=team_member_info,
|
value=team_member_info,
|
||||||
|
|
@ -2149,6 +2158,7 @@ async def _user_api_key_auth_builder(
|
||||||
proxy_logging_obj=proxy_logging_obj,
|
proxy_logging_obj=proxy_logging_obj,
|
||||||
)
|
)
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
|
token_team_models: Final = _token_team_models(valid_token)
|
||||||
_team_obj = LiteLLM_TeamTableCachedObj(
|
_team_obj = LiteLLM_TeamTableCachedObj(
|
||||||
team_id=valid_token.team_id,
|
team_id=valid_token.team_id,
|
||||||
max_budget=valid_token.team_max_budget,
|
max_budget=valid_token.team_max_budget,
|
||||||
|
|
@ -2157,7 +2167,7 @@ async def _user_api_key_auth_builder(
|
||||||
tpm_limit=valid_token.team_tpm_limit,
|
tpm_limit=valid_token.team_tpm_limit,
|
||||||
rpm_limit=valid_token.team_rpm_limit,
|
rpm_limit=valid_token.team_rpm_limit,
|
||||||
blocked=valid_token.team_blocked,
|
blocked=valid_token.team_blocked,
|
||||||
models=valid_token.team_models,
|
models=token_team_models,
|
||||||
metadata=valid_token.team_metadata,
|
metadata=valid_token.team_metadata,
|
||||||
object_permission_id=valid_token.team_object_permission_id,
|
object_permission_id=valid_token.team_object_permission_id,
|
||||||
object_permission=await _resolve_object_permission_for_unresolvable_team(
|
object_permission=await _resolve_object_permission_for_unresolvable_team(
|
||||||
|
|
@ -2301,6 +2311,7 @@ def _team_obj_from_token(valid_token: UserAPIKeyAuth) -> LiteLLM_TeamTableCached
|
||||||
UserAPIKeyAuth. Only called when valid_token.team_id is known to be
|
UserAPIKeyAuth. Only called when valid_token.team_id is known to be
|
||||||
non-None (the caller gates on it)."""
|
non-None (the caller gates on it)."""
|
||||||
assert valid_token.team_id is not None
|
assert valid_token.team_id is not None
|
||||||
|
token_team_models: Final = _token_team_models(valid_token)
|
||||||
return LiteLLM_TeamTableCachedObj(
|
return LiteLLM_TeamTableCachedObj(
|
||||||
team_id=valid_token.team_id,
|
team_id=valid_token.team_id,
|
||||||
max_budget=valid_token.team_max_budget,
|
max_budget=valid_token.team_max_budget,
|
||||||
|
|
@ -2309,7 +2320,7 @@ def _team_obj_from_token(valid_token: UserAPIKeyAuth) -> LiteLLM_TeamTableCached
|
||||||
tpm_limit=valid_token.team_tpm_limit,
|
tpm_limit=valid_token.team_tpm_limit,
|
||||||
rpm_limit=valid_token.team_rpm_limit,
|
rpm_limit=valid_token.team_rpm_limit,
|
||||||
blocked=valid_token.team_blocked,
|
blocked=valid_token.team_blocked,
|
||||||
models=valid_token.team_models,
|
models=token_team_models,
|
||||||
metadata=valid_token.team_metadata,
|
metadata=valid_token.team_metadata,
|
||||||
object_permission_id=valid_token.team_object_permission_id,
|
object_permission_id=valid_token.team_object_permission_id,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ from dataclasses import asdict, dataclass
|
||||||
from typing import TYPE_CHECKING, Final, Protocol, cast # noqa: TID251 # untyped prisma/redis boundary needs cast
|
from typing import TYPE_CHECKING, Final, Protocol, cast # noqa: TID251 # untyped prisma/redis boundary needs cast
|
||||||
|
|
||||||
from litellm._logging import verbose_proxy_logger
|
from litellm._logging import verbose_proxy_logger
|
||||||
|
from litellm.repositories.prisma_protocols import RowT_co, TableActions
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from litellm.caching.redis_cache import RedisCache
|
from litellm.caching.redis_cache import RedisCache
|
||||||
|
|
@ -163,13 +164,14 @@ class _PublishOnWriteActions:
|
||||||
|
|
||||||
|
|
||||||
def wrap_table_actions_for_config_sync(
|
def wrap_table_actions_for_config_sync(
|
||||||
actions: object,
|
actions: "TableActions[RowT_co]",
|
||||||
table_name: str,
|
table_name: str,
|
||||||
publish: Callable[[str], Awaitable[None]] = publish_config_change_for_object_type,
|
publish: Callable[[str], Awaitable[None]] = publish_config_change_for_object_type,
|
||||||
) -> object:
|
) -> "TableActions[RowT_co]":
|
||||||
if table_name not in _CONFIG_SYNCED_TABLE_NAMES:
|
if table_name not in _CONFIG_SYNCED_TABLE_NAMES:
|
||||||
return actions
|
return actions
|
||||||
return _PublishOnWriteActions(actions=actions, object_type=table_name, publish=publish)
|
wrapped: Final = _PublishOnWriteActions(actions=actions, object_type=table_name, publish=publish)
|
||||||
|
return cast("TableActions[RowT_co]", wrapped) # cast-ok: dynamic write-through proxy keeps the wrapped row type
|
||||||
|
|
||||||
|
|
||||||
class ConfigSyncSubscriber:
|
class ConfigSyncSubscriber:
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,9 @@ Expired UI session key cleanup manager.
|
||||||
Deletes expired virtual keys created for LiteLLM dashboard sessions.
|
Deletes expired virtual keys created for LiteLLM dashboard sessions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from collections.abc import Sequence
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Any, Final
|
from typing import Any, Final, Protocol
|
||||||
|
|
||||||
from litellm._logging import verbose_proxy_logger
|
from litellm._logging import verbose_proxy_logger
|
||||||
from litellm.constants import (
|
from litellm.constants import (
|
||||||
|
|
@ -14,7 +15,7 @@ from litellm.constants import (
|
||||||
LITELLM_INTERNAL_JOBS_SERVICE_ACCOUNT_NAME,
|
LITELLM_INTERNAL_JOBS_SERVICE_ACCOUNT_NAME,
|
||||||
UI_SESSION_TOKEN_TEAM_ID,
|
UI_SESSION_TOKEN_TEAM_ID,
|
||||||
)
|
)
|
||||||
from litellm.proxy._types import KeyRequest, LiteLLM_VerificationToken, UserAPIKeyAuth
|
from litellm.proxy._types import KeyRequest, UserAPIKeyAuth
|
||||||
from litellm.proxy.common_utils.user_api_key_cache import UserApiKeyCache
|
from litellm.proxy.common_utils.user_api_key_cache import UserApiKeyCache
|
||||||
from litellm.proxy.hooks.key_management_event_hooks import KeyManagementEventHooks
|
from litellm.proxy.hooks.key_management_event_hooks import KeyManagementEventHooks
|
||||||
from litellm.proxy.management_endpoints.key_management_endpoints import (
|
from litellm.proxy.management_endpoints.key_management_endpoints import (
|
||||||
|
|
@ -26,6 +27,11 @@ from litellm.repositories.verification_token_repository import (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _ExpiredSessionKeyRow(Protocol):
|
||||||
|
@property
|
||||||
|
def token(self) -> str | None: ...
|
||||||
|
|
||||||
|
|
||||||
class ExpiredUISessionKeyCleanupManager:
|
class ExpiredUISessionKeyCleanupManager:
|
||||||
"""
|
"""
|
||||||
Cleans up expired UI session keys.
|
Cleans up expired UI session keys.
|
||||||
|
|
@ -138,7 +144,7 @@ class ExpiredUISessionKeyCleanupManager:
|
||||||
|
|
||||||
return len(tokens)
|
return len(tokens)
|
||||||
|
|
||||||
async def _find_expired_ui_session_keys(self) -> list[LiteLLM_VerificationToken]:
|
async def _find_expired_ui_session_keys(self) -> Sequence[_ExpiredSessionKeyRow]:
|
||||||
"""
|
"""
|
||||||
Find expired LiteLLM dashboard session keys.
|
Find expired LiteLLM dashboard session keys.
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,9 @@ Key Rotation Manager - Automated key rotation based on rotation schedules
|
||||||
Handles finding keys that need rotation based on their individual schedules.
|
Handles finding keys that need rotation based on their individual schedules.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from collections.abc import Sequence
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Final
|
from typing import TYPE_CHECKING, Final
|
||||||
|
|
||||||
from litellm._logging import verbose_proxy_logger
|
from litellm._logging import verbose_proxy_logger
|
||||||
from litellm.constants import (
|
from litellm.constants import (
|
||||||
|
|
@ -31,6 +32,9 @@ from litellm.repositories.verification_token_repository import (
|
||||||
VerificationTokenRepository,
|
VerificationTokenRepository,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from prisma import models as prisma_models
|
||||||
|
|
||||||
|
|
||||||
class KeyRotationManager:
|
class KeyRotationManager:
|
||||||
"""
|
"""
|
||||||
|
|
@ -106,7 +110,7 @@ class KeyRotationManager:
|
||||||
cronjob_id=KEY_ROTATION_JOB_NAME,
|
cronjob_id=KEY_ROTATION_JOB_NAME,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _find_keys_needing_rotation(self) -> list[LiteLLM_VerificationToken]:
|
async def _find_keys_needing_rotation(self) -> "Sequence[prisma_models.LiteLLM_VerificationToken]":
|
||||||
"""
|
"""
|
||||||
Find keys that are due for rotation based on their key_rotation_at timestamp.
|
Find keys that are due for rotation based on their key_rotation_at timestamp.
|
||||||
|
|
||||||
|
|
@ -156,7 +160,7 @@ class KeyRotationManager:
|
||||||
# Check if the rotation time has passed
|
# Check if the rotation time has passed
|
||||||
return now >= key.key_rotation_at
|
return now >= key.key_rotation_at
|
||||||
|
|
||||||
async def _rotate_key(self, key: LiteLLM_VerificationToken):
|
async def _rotate_key(self, key: "prisma_models.LiteLLM_VerificationToken"):
|
||||||
"""
|
"""
|
||||||
Rotate a single key using existing regenerate_key_fn and call the rotation hook
|
Rotate a single key using existing regenerate_key_fn and call the rotation hook
|
||||||
"""
|
"""
|
||||||
|
|
@ -197,7 +201,7 @@ class KeyRotationManager:
|
||||||
if isinstance(response, GenerateKeyResponse):
|
if isinstance(response, GenerateKeyResponse):
|
||||||
await KeyManagementEventHooks.async_key_rotated_hook(
|
await KeyManagementEventHooks.async_key_rotated_hook(
|
||||||
data=regenerate_request,
|
data=regenerate_request,
|
||||||
existing_key_row=key,
|
existing_key_row=key, # pyright: ignore[reportArgumentType] # prisma row, hook wants the domain model
|
||||||
response=response,
|
response=response,
|
||||||
user_api_key_dict=system_user,
|
user_api_key_dict=system_user,
|
||||||
litellm_changed_by=LITELLM_INTERNAL_JOBS_SERVICE_ACCOUNT_NAME,
|
litellm_changed_by=LITELLM_INTERNAL_JOBS_SERVICE_ACCOUNT_NAME,
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ from litellm.proxy.db.db_transaction_queue.pod_lock_manager import PodLockManage
|
||||||
from litellm.proxy.db.exception_handler import call_with_db_reconnect_retry
|
from litellm.proxy.db.exception_handler import call_with_db_reconnect_retry
|
||||||
from litellm.proxy.utils import PrismaClient, ProxyLogging
|
from litellm.proxy.utils import PrismaClient, ProxyLogging
|
||||||
from litellm.repositories.organization_repository import OrganizationRepository
|
from litellm.repositories.organization_repository import OrganizationRepository
|
||||||
from litellm.repositories.prisma_protocols import ReadOnlyTable, SpendLinkedTable
|
from litellm.repositories.prisma_protocols import SpendLinkedTable
|
||||||
from litellm.repositories.table_repositories import (
|
from litellm.repositories.table_repositories import (
|
||||||
EndUserRepository,
|
EndUserRepository,
|
||||||
TagRepository,
|
TagRepository,
|
||||||
|
|
@ -675,7 +675,7 @@ class ResetBudgetJob:
|
||||||
rely on the default budget (litellm.max_end_user_budget_id) applied
|
rely on the default budget (litellm.max_end_user_budget_id) applied
|
||||||
in-memory during auth checks.
|
in-memory during auth checks.
|
||||||
"""
|
"""
|
||||||
table: Final[ReadOnlyTable] = EndUserRepository(self.prisma_client).table
|
table: Final = EndUserRepository(self.prisma_client).table
|
||||||
rows: Final = await self._with_db_retry(
|
rows: Final = await self._with_db_retry(
|
||||||
lambda: table.find_many(
|
lambda: table.find_many(
|
||||||
where={
|
where={
|
||||||
|
|
@ -685,7 +685,7 @@ class ResetBudgetJob:
|
||||||
),
|
),
|
||||||
reason="reset_budget_read_endusers_without_budget_id_failure",
|
reason="reset_budget_read_endusers_without_budget_id_failure",
|
||||||
)
|
)
|
||||||
return [LiteLLM_EndUserTable.model_validate(row.dict()) for row in rows]
|
return [LiteLLM_EndUserTable.model_validate(row.model_dump()) for row in rows]
|
||||||
|
|
||||||
async def _write_key_reset_updates(self, updated_keys: list[LiteLLM_VerificationToken]) -> None:
|
async def _write_key_reset_updates(self, updated_keys: list[LiteLLM_VerificationToken]) -> None:
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import json
|
import json
|
||||||
from collections.abc import Mapping, Sequence
|
from collections.abc import Mapping, Sequence
|
||||||
from collections.abc import Set as AbstractSet
|
from collections.abc import Set as AbstractSet
|
||||||
from typing import TYPE_CHECKING, Any, Final, Protocol
|
from typing import TYPE_CHECKING, Any, Final
|
||||||
|
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
|
|
||||||
|
|
@ -18,28 +18,11 @@ from litellm.repositories.table_repositories import ManagedObjectRepository
|
||||||
from litellm.responses.utils import ResponsesAPIRequestUtils
|
from litellm.responses.utils import ResponsesAPIRequestUtils
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from prisma import models as prisma_models
|
||||||
|
|
||||||
from litellm.proxy.utils import PrismaClient
|
from litellm.proxy.utils import PrismaClient
|
||||||
|
|
||||||
|
|
||||||
class _ManagedObjectRow(Protocol):
|
|
||||||
model_object_id: str
|
|
||||||
unified_object_id: str | None
|
|
||||||
file_purpose: str | None
|
|
||||||
created_by: str | None
|
|
||||||
|
|
||||||
|
|
||||||
class _ManagedObjectTable(Protocol):
|
|
||||||
async def find_unique(self, *, where: Mapping[str, str]) -> _ManagedObjectRow | None: ...
|
|
||||||
|
|
||||||
async def find_first(self, *, where: Mapping[str, str]) -> _ManagedObjectRow | None: ...
|
|
||||||
|
|
||||||
async def find_many(self, *, where: Mapping[str, object]) -> Sequence[_ManagedObjectRow]: ...
|
|
||||||
|
|
||||||
async def create(self, *, data: Mapping[str, str]) -> _ManagedObjectRow: ...
|
|
||||||
|
|
||||||
async def update(self, *, where: Mapping[str, str], data: Mapping[str, str]) -> _ManagedObjectRow | None: ...
|
|
||||||
|
|
||||||
|
|
||||||
CONTAINER_OBJECT_PURPOSE: Final = "container"
|
CONTAINER_OBJECT_PURPOSE: Final = "container"
|
||||||
|
|
||||||
# 60s LRU/TTL cache absorbs every container access check before it reaches
|
# 60s LRU/TTL cache absorbs every container access check before it reaches
|
||||||
|
|
@ -220,7 +203,7 @@ async def record_container_owner(
|
||||||
verbose_proxy_logger.warning("Skipping container ownership tracking because prisma_client is None")
|
verbose_proxy_logger.warning("Skipping container ownership tracking because prisma_client is None")
|
||||||
return response
|
return response
|
||||||
|
|
||||||
table: Final[_ManagedObjectTable] = ManagedObjectRepository(prisma_client).table
|
table: Final = ManagedObjectRepository(prisma_client).table
|
||||||
existing: Final = await table.find_unique(where={"model_object_id": model_object_id})
|
existing: Final = await table.find_unique(where={"model_object_id": model_object_id})
|
||||||
if existing is not None:
|
if existing is not None:
|
||||||
if getattr(existing, "file_purpose", None) != CONTAINER_OBJECT_PURPOSE:
|
if getattr(existing, "file_purpose", None) != CONTAINER_OBJECT_PURPOSE:
|
||||||
|
|
@ -272,8 +255,8 @@ async def _get_container_owner(original_container_id: str, custom_llm_provider:
|
||||||
if prisma_client is None:
|
if prisma_client is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
table: Final[_ManagedObjectTable] = ManagedObjectRepository(prisma_client).table
|
table: Final = ManagedObjectRepository(prisma_client).table
|
||||||
row: Final[_ManagedObjectRow | None] = await table.find_first(
|
row: Final[prisma_models.LiteLLM_ManagedObjectTable | None] = await table.find_first(
|
||||||
where={
|
where={
|
||||||
"model_object_id": model_object_id,
|
"model_object_id": model_object_id,
|
||||||
"file_purpose": CONTAINER_OBJECT_PURPOSE,
|
"file_purpose": CONTAINER_OBJECT_PURPOSE,
|
||||||
|
|
@ -309,8 +292,8 @@ async def _get_stored_container_id(original_container_id: str, custom_llm_provid
|
||||||
if prisma_client is None:
|
if prisma_client is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
table: Final[_ManagedObjectTable] = ManagedObjectRepository(prisma_client).table
|
table: Final = ManagedObjectRepository(prisma_client).table
|
||||||
row: Final[_ManagedObjectRow | None] = await table.find_first(
|
row: Final[prisma_models.LiteLLM_ManagedObjectTable | None] = await table.find_first(
|
||||||
where={
|
where={
|
||||||
"model_object_id": model_object_id,
|
"model_object_id": model_object_id,
|
||||||
"file_purpose": CONTAINER_OBJECT_PURPOSE,
|
"file_purpose": CONTAINER_OBJECT_PURPOSE,
|
||||||
|
|
@ -394,8 +377,8 @@ async def _get_allowed_container_ids(
|
||||||
if prisma_client is None:
|
if prisma_client is None:
|
||||||
return set()
|
return set()
|
||||||
|
|
||||||
table: Final[_ManagedObjectTable] = ManagedObjectRepository(prisma_client).table
|
table: Final = ManagedObjectRepository(prisma_client).table
|
||||||
rows: Final[Sequence[_ManagedObjectRow]] = await table.find_many(
|
rows: Final[Sequence[prisma_models.LiteLLM_ManagedObjectTable]] = await table.find_many(
|
||||||
where={
|
where={
|
||||||
"file_purpose": CONTAINER_OBJECT_PURPOSE,
|
"file_purpose": CONTAINER_OBJECT_PURPOSE,
|
||||||
"created_by": {"in": owner_scopes},
|
"created_by": {"in": owner_scopes},
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,10 @@
|
||||||
CRUD endpoints for storing reusable credentials.
|
CRUD endpoints for storing reusable credentials.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Final
|
from typing import (
|
||||||
|
Final,
|
||||||
|
cast, # noqa: TID251 # jsonify_object in proxy/utils.py is annotated with a bare dict
|
||||||
|
)
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Path, Request, Response
|
from fastapi import APIRouter, Depends, HTTPException, Path, Request, Response
|
||||||
|
|
||||||
|
|
@ -88,7 +91,9 @@ async def create_credential(
|
||||||
)
|
)
|
||||||
encrypted_credential: Final = CredentialHelperUtils.encrypt_credential_values(processed_credential)
|
encrypted_credential: Final = CredentialHelperUtils.encrypt_credential_values(processed_credential)
|
||||||
credentials_dict: Final = encrypted_credential.model_dump()
|
credentials_dict: Final = encrypted_credential.model_dump()
|
||||||
credentials_dict_jsonified: Final = jsonify_object(credentials_dict)
|
credentials_dict_jsonified: Final = cast( # cast-ok: deep-copies a model_dump, so keys are str
|
||||||
|
"dict[str, object]", jsonify_object(credentials_dict)
|
||||||
|
)
|
||||||
await CredentialsRepository(prisma_client).create(
|
await CredentialsRepository(prisma_client).create(
|
||||||
data={
|
data={
|
||||||
**credentials_dict_jsonified,
|
**credentials_dict_jsonified,
|
||||||
|
|
@ -310,7 +315,9 @@ async def update_credential(
|
||||||
if db_credential is None:
|
if db_credential is None:
|
||||||
raise HTTPException(status_code=404, detail="Credential not found in DB.")
|
raise HTTPException(status_code=404, detail="Credential not found in DB.")
|
||||||
merged_credential: Final = update_db_credential(db_credential, credential)
|
merged_credential: Final = update_db_credential(db_credential, credential)
|
||||||
credential_object_jsonified: Final = jsonify_object(merged_credential.model_dump())
|
credential_object_jsonified: Final = cast( # cast-ok: deep-copies a model_dump, so keys are str
|
||||||
|
"dict[str, object]", jsonify_object(merged_credential.model_dump())
|
||||||
|
)
|
||||||
await credentials_repository.update_by_name(
|
await credentials_repository.update_by_name(
|
||||||
credential_name,
|
credential_name,
|
||||||
data={
|
data={
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,15 @@ Admins use the management endpoints to read and update input_policy / output_pol
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
from collections.abc import Mapping, Sequence
|
from collections.abc import Mapping
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import TYPE_CHECKING, Any, Final, Protocol, TypeVar
|
from typing import TYPE_CHECKING, Any, Final
|
||||||
|
|
||||||
from litellm._logging import verbose_proxy_logger
|
from litellm._logging import verbose_proxy_logger
|
||||||
from litellm.proxy._types import ToolDiscoveryQueueItem
|
from litellm.proxy._types import ToolDiscoveryQueueItem
|
||||||
from litellm.proxy.db.exception_handler import call_with_db_reconnect_retry
|
from litellm.proxy.db.exception_handler import call_with_db_reconnect_retry
|
||||||
from litellm.repositories.object_permission_repository import ObjectPermissionRepository
|
from litellm.repositories.object_permission_repository import ObjectPermissionRepository
|
||||||
|
from litellm.repositories.prisma_protocols import TableActions
|
||||||
from litellm.repositories.table_repositories import ToolRepository
|
from litellm.repositories.table_repositories import ToolRepository
|
||||||
from litellm.types.tool_management import (
|
from litellm.types.tool_management import (
|
||||||
LiteLLM_ToolTableRow,
|
LiteLLM_ToolTableRow,
|
||||||
|
|
@ -25,33 +26,16 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
from litellm.proxy.utils import PrismaClient
|
from litellm.proxy.utils import PrismaClient
|
||||||
|
|
||||||
_RowT_co: Final = TypeVar("_RowT_co", covariant=True)
|
|
||||||
|
|
||||||
|
def _tool_table_actions(prisma_client: "PrismaClient") -> "TableActions[prisma_db_models.LiteLLM_ToolTable]":
|
||||||
class _TableActions(Protocol[_RowT_co]):
|
table: Final[TableActions[prisma_db_models.LiteLLM_ToolTable]] = ToolRepository(prisma_client).table
|
||||||
async def find_unique(self, where: Mapping[str, object]) -> _RowT_co | None: ...
|
|
||||||
|
|
||||||
async def find_many(
|
|
||||||
self,
|
|
||||||
where: Mapping[str, object] | None = None,
|
|
||||||
order: Mapping[str, object] | None = None,
|
|
||||||
include: Mapping[str, object] | None = None,
|
|
||||||
) -> Sequence[_RowT_co]: ...
|
|
||||||
|
|
||||||
async def upsert(self, where: Mapping[str, object], data: Mapping[str, object]) -> _RowT_co: ...
|
|
||||||
|
|
||||||
async def update(self, where: Mapping[str, object], data: Mapping[str, object]) -> _RowT_co | None: ...
|
|
||||||
|
|
||||||
|
|
||||||
def _tool_table_actions(prisma_client: "PrismaClient") -> "_TableActions[prisma_db_models.LiteLLM_ToolTable]":
|
|
||||||
table: Final[_TableActions[prisma_db_models.LiteLLM_ToolTable]] = ToolRepository(prisma_client).table
|
|
||||||
return table
|
return table
|
||||||
|
|
||||||
|
|
||||||
def _object_permission_table_actions(
|
def _object_permission_table_actions(
|
||||||
prisma_client: "PrismaClient",
|
prisma_client: "PrismaClient",
|
||||||
) -> "_TableActions[prisma_db_models.LiteLLM_ObjectPermissionTable]":
|
) -> "TableActions[prisma_db_models.LiteLLM_ObjectPermissionTable]":
|
||||||
table: Final[_TableActions[prisma_db_models.LiteLLM_ObjectPermissionTable]] = ObjectPermissionRepository(
|
table: Final[TableActions[prisma_db_models.LiteLLM_ObjectPermissionTable]] = ObjectPermissionRepository(
|
||||||
prisma_client
|
prisma_client
|
||||||
).table
|
).table
|
||||||
return table
|
return table
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ from litellm.proxy.guardrails.guardrail_hooks.custom_code.sandbox import (
|
||||||
from litellm.proxy.guardrails.guardrail_registry import GuardrailRegistry
|
from litellm.proxy.guardrails.guardrail_registry import GuardrailRegistry
|
||||||
from litellm.proxy.guardrails.usage_endpoints import router as guardrails_usage_router
|
from litellm.proxy.guardrails.usage_endpoints import router as guardrails_usage_router
|
||||||
from litellm.proxy.management_endpoints.common_utils import _user_has_admin_view
|
from litellm.proxy.management_endpoints.common_utils import _user_has_admin_view
|
||||||
|
from litellm.repositories.prisma_protocols import TableActions
|
||||||
from litellm.repositories.table_repositories import GuardrailsRepository
|
from litellm.repositories.table_repositories import GuardrailsRepository
|
||||||
from litellm.types.guardrails import (
|
from litellm.types.guardrails import (
|
||||||
PII_ENTITY_CATEGORIES_MAP,
|
PII_ENTITY_CATEGORIES_MAP,
|
||||||
|
|
@ -65,29 +66,12 @@ router: Final = APIRouter()
|
||||||
GUARDRAIL_REGISTRY: Final = GuardrailRegistry()
|
GUARDRAIL_REGISTRY: Final = GuardrailRegistry()
|
||||||
|
|
||||||
|
|
||||||
class _GuardrailsTableActions(Protocol):
|
|
||||||
async def create(self, data: Mapping[str, object]) -> "LiteLLM_GuardrailsTable": ...
|
|
||||||
|
|
||||||
async def delete(self, where: Mapping[str, object]) -> "LiteLLM_GuardrailsTable | None": ...
|
|
||||||
|
|
||||||
async def find_unique(self, where: Mapping[str, object]) -> "LiteLLM_GuardrailsTable | None": ...
|
|
||||||
|
|
||||||
async def find_many(
|
|
||||||
self, where: Mapping[str, object], order: Mapping[str, str]
|
|
||||||
) -> "Sequence[LiteLLM_GuardrailsTable]": ...
|
|
||||||
|
|
||||||
async def update(
|
|
||||||
self, where: Mapping[str, object], data: Mapping[str, object]
|
|
||||||
) -> "LiteLLM_GuardrailsTable | None": ...
|
|
||||||
|
|
||||||
|
|
||||||
def _as_str_object_mapping(mapping: Mapping[str, object]) -> Mapping[str, object]:
|
def _as_str_object_mapping(mapping: Mapping[str, object]) -> Mapping[str, object]:
|
||||||
return mapping
|
return mapping
|
||||||
|
|
||||||
|
|
||||||
def _guardrails_table(prisma_client: "PrismaClient") -> _GuardrailsTableActions:
|
def _guardrails_table(prisma_client: "PrismaClient") -> "TableActions[LiteLLM_GuardrailsTable]":
|
||||||
table: Final[_GuardrailsTableActions] = GuardrailsRepository(prisma_client).table
|
return GuardrailsRepository(prisma_client).table
|
||||||
return table
|
|
||||||
|
|
||||||
|
|
||||||
async def _create_guardrail_row(prisma_client: "PrismaClient", data: Mapping[str, object]) -> "LiteLLM_GuardrailsTable":
|
async def _create_guardrail_row(prisma_client: "PrismaClient", data: Mapping[str, object]) -> "LiteLLM_GuardrailsTable":
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,12 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import importlib
|
import importlib
|
||||||
import os
|
import os
|
||||||
from collections.abc import Callable, Iterator, Mapping, Sequence
|
from collections.abc import Callable, Iterator, Mapping
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from itertools import chain, count
|
from itertools import chain, count
|
||||||
from typing import Final, Literal, Optional, Protocol, cast
|
from typing import TYPE_CHECKING, Final, Literal, Optional, Protocol, cast
|
||||||
|
|
||||||
from pydantic import BaseModel, ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
||||||
import litellm
|
import litellm
|
||||||
from litellm import Router
|
from litellm import Router
|
||||||
|
|
@ -39,6 +39,7 @@ from litellm.proxy.guardrails.guardrail_hooks.tool_permission import (
|
||||||
)
|
)
|
||||||
from litellm.proxy.types_utils.utils import get_instance_fn
|
from litellm.proxy.types_utils.utils import get_instance_fn
|
||||||
from litellm.proxy.utils import PrismaClient
|
from litellm.proxy.utils import PrismaClient
|
||||||
|
from litellm.repositories.prisma_protocols import TableActions
|
||||||
from litellm.repositories.table_repositories import GuardrailsRepository
|
from litellm.repositories.table_repositories import GuardrailsRepository
|
||||||
from litellm.secret_managers.main import get_secret
|
from litellm.secret_managers.main import get_secret
|
||||||
from litellm.types.guardrails import (
|
from litellm.types.guardrails import (
|
||||||
|
|
@ -61,6 +62,9 @@ from .guardrail_initializers import (
|
||||||
initialize_tool_permission,
|
initialize_tool_permission,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from prisma import models as prisma_models
|
||||||
|
|
||||||
|
|
||||||
class _GuardrailRowLike(Protocol):
|
class _GuardrailRowLike(Protocol):
|
||||||
@property
|
@property
|
||||||
|
|
@ -68,15 +72,7 @@ class _GuardrailRowLike(Protocol):
|
||||||
def __iter__(self) -> Iterator[tuple[str, object]]: ...
|
def __iter__(self) -> Iterator[tuple[str, object]]: ...
|
||||||
|
|
||||||
|
|
||||||
class _GuardrailTableActions(Protocol):
|
def _guardrail_table(prisma_client: PrismaClient) -> "TableActions[prisma_models.LiteLLM_GuardrailsTable]":
|
||||||
async def create(self, *, data: Mapping[str, object]) -> _GuardrailRowLike: ...
|
|
||||||
async def delete(self, *, where: Mapping[str, str]) -> object: ...
|
|
||||||
async def update(self, *, where: Mapping[str, str], data: Mapping[str, object]) -> _GuardrailRowLike: ...
|
|
||||||
async def find_many(self, *, where: Mapping[str, str], order: Mapping[str, str]) -> Sequence[BaseModel]: ...
|
|
||||||
async def find_unique(self, *, where: Mapping[str, str]) -> BaseModel | None: ...
|
|
||||||
|
|
||||||
|
|
||||||
def _guardrail_table(prisma_client: PrismaClient) -> _GuardrailTableActions:
|
|
||||||
"""Typed view of the guardrails table actions exposed by the Prisma repository."""
|
"""Typed view of the guardrails table actions exposed by the Prisma repository."""
|
||||||
return GuardrailsRepository(prisma_client).table
|
return GuardrailsRepository(prisma_client).table
|
||||||
|
|
||||||
|
|
@ -347,7 +343,7 @@ class GuardrailRegistry:
|
||||||
guardrail_info: Final[str] = safe_dumps(guardrail.get("guardrail_info", {}))
|
guardrail_info: Final[str] = safe_dumps(guardrail.get("guardrail_info", {}))
|
||||||
|
|
||||||
# Update in DB
|
# Update in DB
|
||||||
updated_guardrail: Final[_GuardrailRowLike] = await _guardrail_table(prisma_client).update(
|
updated_guardrail: Final[_GuardrailRowLike | None] = await _guardrail_table(prisma_client).update(
|
||||||
where={"guardrail_id": guardrail_id},
|
where={"guardrail_id": guardrail_id},
|
||||||
data={
|
data={
|
||||||
"guardrail_name": guardrail_name,
|
"guardrail_name": guardrail_name,
|
||||||
|
|
@ -356,6 +352,8 @@ class GuardrailRegistry:
|
||||||
"updated_at": datetime.now(timezone.utc),
|
"updated_at": datetime.now(timezone.utc),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
if updated_guardrail is None:
|
||||||
|
raise ValueError(f"Guardrail not found, passed guardrail_id={guardrail_id}")
|
||||||
|
|
||||||
# Convert to dict and return
|
# Convert to dict and return
|
||||||
return dict(updated_guardrail)
|
return dict(updated_guardrail)
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ from typing_extensions import NotRequired, ReadOnly, TypedDict
|
||||||
from litellm._logging import verbose_proxy_logger
|
from litellm._logging import verbose_proxy_logger
|
||||||
from litellm.proxy._types import UserAPIKeyAuth
|
from litellm.proxy._types import UserAPIKeyAuth
|
||||||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||||
|
from litellm.repositories.prisma_protocols import TableActions
|
||||||
from litellm.repositories.table_repositories import (
|
from litellm.repositories.table_repositories import (
|
||||||
DailyGuardrailMetricsRepository,
|
DailyGuardrailMetricsRepository,
|
||||||
DailyGuardrailUsageUnitsRepository,
|
DailyGuardrailUsageUnitsRepository,
|
||||||
|
|
@ -30,13 +31,6 @@ from litellm.repositories.table_repositories import (
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from prisma import models as prisma_models
|
from prisma import models as prisma_models
|
||||||
from prisma import types as prisma_types
|
from prisma import types as prisma_types
|
||||||
from prisma.actions import (
|
|
||||||
LiteLLM_DailyGuardrailMetricsActions,
|
|
||||||
LiteLLM_DailyGuardrailUsageUnitsActions,
|
|
||||||
LiteLLM_DailyPolicyMetricsActions,
|
|
||||||
LiteLLM_GuardrailsTableActions,
|
|
||||||
LiteLLM_PolicyTableActions,
|
|
||||||
)
|
|
||||||
|
|
||||||
from litellm.proxy.utils import PrismaClient
|
from litellm.proxy.utils import PrismaClient
|
||||||
from litellm.types.guardrails import Guardrail
|
from litellm.types.guardrails import Guardrail
|
||||||
|
|
@ -85,8 +79,8 @@ def _resolve_usage_window(start_date: str | None, end_date: str | None) -> tuple
|
||||||
|
|
||||||
def _guardrails_table(
|
def _guardrails_table(
|
||||||
prisma_client: "PrismaClient",
|
prisma_client: "PrismaClient",
|
||||||
) -> "LiteLLM_GuardrailsTableActions[prisma_models.LiteLLM_GuardrailsTable]":
|
) -> "TableActions[prisma_models.LiteLLM_GuardrailsTable]":
|
||||||
guardrails_table: LiteLLM_GuardrailsTableActions[prisma_models.LiteLLM_GuardrailsTable] = GuardrailsRepository(
|
guardrails_table: Final[TableActions[prisma_models.LiteLLM_GuardrailsTable]] = GuardrailsRepository(
|
||||||
prisma_client
|
prisma_client
|
||||||
).table
|
).table
|
||||||
return guardrails_table
|
return guardrails_table
|
||||||
|
|
@ -94,28 +88,26 @@ def _guardrails_table(
|
||||||
|
|
||||||
def _policies_table(
|
def _policies_table(
|
||||||
prisma_client: "PrismaClient",
|
prisma_client: "PrismaClient",
|
||||||
) -> "LiteLLM_PolicyTableActions[prisma_models.LiteLLM_PolicyTable]":
|
) -> "TableActions[prisma_models.LiteLLM_PolicyTable]":
|
||||||
policies_table: Final[LiteLLM_PolicyTableActions[prisma_models.LiteLLM_PolicyTable]] = PolicyRepository(
|
policies_table: Final[TableActions[prisma_models.LiteLLM_PolicyTable]] = PolicyRepository(prisma_client).table
|
||||||
prisma_client
|
|
||||||
).table
|
|
||||||
return policies_table
|
return policies_table
|
||||||
|
|
||||||
|
|
||||||
def _daily_guardrail_metrics_table(
|
def _daily_guardrail_metrics_table(
|
||||||
prisma_client: "PrismaClient",
|
prisma_client: "PrismaClient",
|
||||||
) -> "LiteLLM_DailyGuardrailMetricsActions[prisma_models.LiteLLM_DailyGuardrailMetrics]":
|
) -> "TableActions[prisma_models.LiteLLM_DailyGuardrailMetrics]":
|
||||||
metrics_table: Final[LiteLLM_DailyGuardrailMetricsActions[prisma_models.LiteLLM_DailyGuardrailMetrics]] = (
|
metrics_table: Final[TableActions[prisma_models.LiteLLM_DailyGuardrailMetrics]] = DailyGuardrailMetricsRepository(
|
||||||
DailyGuardrailMetricsRepository(prisma_client).table
|
prisma_client
|
||||||
)
|
).table
|
||||||
return metrics_table
|
return metrics_table
|
||||||
|
|
||||||
|
|
||||||
def _daily_policy_metrics_table(
|
def _daily_policy_metrics_table(
|
||||||
prisma_client: "PrismaClient",
|
prisma_client: "PrismaClient",
|
||||||
) -> "LiteLLM_DailyPolicyMetricsActions[prisma_models.LiteLLM_DailyPolicyMetrics]":
|
) -> "TableActions[prisma_models.LiteLLM_DailyPolicyMetrics]":
|
||||||
metrics_table: Final[LiteLLM_DailyPolicyMetricsActions[prisma_models.LiteLLM_DailyPolicyMetrics]] = (
|
metrics_table: Final[TableActions[prisma_models.LiteLLM_DailyPolicyMetrics]] = DailyPolicyMetricsRepository(
|
||||||
DailyPolicyMetricsRepository(prisma_client).table
|
prisma_client
|
||||||
)
|
).table
|
||||||
return metrics_table
|
return metrics_table
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -135,8 +127,8 @@ async def _find_daily_policy_metrics(
|
||||||
|
|
||||||
def _daily_guardrail_usage_units_table(
|
def _daily_guardrail_usage_units_table(
|
||||||
prisma_client: "PrismaClient",
|
prisma_client: "PrismaClient",
|
||||||
) -> "LiteLLM_DailyGuardrailUsageUnitsActions[prisma_models.LiteLLM_DailyGuardrailUsageUnits]":
|
) -> "TableActions[prisma_models.LiteLLM_DailyGuardrailUsageUnits]":
|
||||||
units_table: Final[LiteLLM_DailyGuardrailUsageUnitsActions[prisma_models.LiteLLM_DailyGuardrailUsageUnits]] = (
|
units_table: Final[TableActions[prisma_models.LiteLLM_DailyGuardrailUsageUnits]] = (
|
||||||
DailyGuardrailUsageUnitsRepository(prisma_client).table
|
DailyGuardrailUsageUnitsRepository(prisma_client).table
|
||||||
)
|
)
|
||||||
return units_table
|
return units_table
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ from operator import itemgetter
|
||||||
from types import MappingProxyType
|
from types import MappingProxyType
|
||||||
from typing import TYPE_CHECKING, Any, Final, NamedTuple, TypeVar
|
from typing import TYPE_CHECKING, Any, Final, NamedTuple, TypeVar
|
||||||
|
|
||||||
|
from typing_extensions import ReadOnly, TypedDict
|
||||||
|
|
||||||
from litellm._logging import verbose_proxy_logger
|
from litellm._logging import verbose_proxy_logger
|
||||||
from litellm.proxy._types import DB_RETRY_SAFE_ERROR_TYPES
|
from litellm.proxy._types import DB_RETRY_SAFE_ERROR_TYPES
|
||||||
from litellm.proxy.utils import PrismaClient
|
from litellm.proxy.utils import PrismaClient
|
||||||
|
|
@ -47,6 +49,18 @@ class _MetricsKey(NamedTuple):
|
||||||
date: str
|
date: str
|
||||||
|
|
||||||
|
|
||||||
|
class _UsageUnitCompoundKey(TypedDict):
|
||||||
|
guardrail_id: ReadOnly[str]
|
||||||
|
date: ReadOnly[str]
|
||||||
|
team_id: ReadOnly[str]
|
||||||
|
api_key: ReadOnly[str]
|
||||||
|
usage_unit: ReadOnly[str]
|
||||||
|
|
||||||
|
|
||||||
|
class _UsageUnitWhereUnique(TypedDict):
|
||||||
|
guardrail_id_date_team_id_api_key_usage_unit: ReadOnly[_UsageUnitCompoundKey]
|
||||||
|
|
||||||
|
|
||||||
class PendingRollups:
|
class PendingRollups:
|
||||||
"""Rollup rows whose connection-error retries exhausted, held for the next flush."""
|
"""Rollup rows whose connection-error retries exhausted, held for the next flush."""
|
||||||
|
|
||||||
|
|
@ -229,7 +243,7 @@ async def _upsert_usage_unit_row(prisma_client: PrismaClient, key: _UsageUnitKey
|
||||||
"usage_unit": key.usage_unit,
|
"usage_unit": key.usage_unit,
|
||||||
"units": units,
|
"units": units,
|
||||||
}
|
}
|
||||||
where: Final[prisma_types.LiteLLM_DailyGuardrailUsageUnitsWhereUniqueInput] = {
|
where: Final[_UsageUnitWhereUnique] = {
|
||||||
"guardrail_id_date_team_id_api_key_usage_unit": {
|
"guardrail_id_date_team_id_api_key_usage_unit": {
|
||||||
"guardrail_id": key.guardrail_id,
|
"guardrail_id": key.guardrail_id,
|
||||||
"date": key.date,
|
"date": key.date,
|
||||||
|
|
|
||||||
|
|
@ -388,7 +388,7 @@ async def list_access_groups(
|
||||||
_require_admin_view(user_api_key_dict)
|
_require_admin_view(user_api_key_dict)
|
||||||
prisma_client: Final = get_prisma_client_or_throw(CommonProxyErrors.db_not_connected_error.value)
|
prisma_client: Final = get_prisma_client_or_throw(CommonProxyErrors.db_not_connected_error.value)
|
||||||
|
|
||||||
table: Final[_AccessGroupTable] = AccessGroupRepository(prisma_client).table
|
table: Final = AccessGroupRepository(prisma_client).table
|
||||||
records: Final = await table.find_many(order={"created_at": "desc"})
|
records: Final = await table.find_many(order={"created_at": "desc"})
|
||||||
return [_record_to_response(r) for r in records]
|
return [_record_to_response(r) for r in records]
|
||||||
|
|
||||||
|
|
@ -404,7 +404,7 @@ async def get_access_group(
|
||||||
_require_admin_view(user_api_key_dict)
|
_require_admin_view(user_api_key_dict)
|
||||||
prisma_client: Final = get_prisma_client_or_throw(CommonProxyErrors.db_not_connected_error.value)
|
prisma_client: Final = get_prisma_client_or_throw(CommonProxyErrors.db_not_connected_error.value)
|
||||||
|
|
||||||
table: Final[_AccessGroupTable] = AccessGroupRepository(prisma_client).table
|
table: Final = AccessGroupRepository(prisma_client).table
|
||||||
record: Final = await table.find_unique(where={"access_group_id": access_group_id})
|
record: Final = await table.find_unique(where={"access_group_id": access_group_id})
|
||||||
if record is None:
|
if record is None:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ async def new_budget(
|
||||||
budget_obj.budget_reset_at = get_budget_reset_time(budget_duration=budget_obj.budget_duration)
|
budget_obj.budget_reset_at = get_budget_reset_time(budget_duration=budget_obj.budget_duration)
|
||||||
|
|
||||||
budget_obj_json: Final = budget_obj.model_dump(exclude_none=True)
|
budget_obj_json: Final = budget_obj.model_dump(exclude_none=True)
|
||||||
budget_obj_jsonified: Final = jsonify_object(budget_obj_json) # json dump any dictionaries
|
budget_obj_jsonified: Final[dict[str, object]] = jsonify_object(budget_obj_json) # mutable-ok: prisma create input
|
||||||
try:
|
try:
|
||||||
response: Final = await BudgetRepository(prisma_client).table.create(
|
response: Final = await BudgetRepository(prisma_client).table.create(
|
||||||
data={
|
data={
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,8 @@ router: Final = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
class _CacheConfigRow(Protocol):
|
class _CacheConfigRow(Protocol):
|
||||||
cache_settings: str | Mapping[str, object] | None
|
@property
|
||||||
|
def cache_settings(self) -> str | Mapping[str, object] | None: ...
|
||||||
|
|
||||||
|
|
||||||
class _CacheConfigTable(Protocol):
|
class _CacheConfigTable(Protocol):
|
||||||
|
|
|
||||||
|
|
@ -441,7 +441,7 @@ async def get_api_key_metadata(
|
||||||
This ensures that key_alias and team_id are preserved in historical activity logs
|
This ensures that key_alias and team_id are preserved in historical activity logs
|
||||||
even after a key is deleted or regenerated.
|
even after a key is deleted or regenerated.
|
||||||
"""
|
"""
|
||||||
key_records: list[PrismaVerificationToken] = await VerificationTokenRepository(prisma_client).table.find_many(
|
key_records: Sequence[PrismaVerificationToken] = await VerificationTokenRepository(prisma_client).table.find_many(
|
||||||
where={"token": {"in": list(api_keys)}}
|
where={"token": {"in": list(api_keys)}}
|
||||||
)
|
)
|
||||||
result: Final[dict[str, _KeyMetadataDict]] = {
|
result: Final[dict[str, _KeyMetadataDict]] = {
|
||||||
|
|
@ -452,9 +452,9 @@ async def get_api_key_metadata(
|
||||||
missing_keys: Final = api_keys - set(result.keys())
|
missing_keys: Final = api_keys - set(result.keys())
|
||||||
if missing_keys:
|
if missing_keys:
|
||||||
try:
|
try:
|
||||||
deleted_key_records: Final[list[PrismaDeletedVerificationToken]] = await DeletedVerificationTokenRepository(
|
deleted_key_records: Final[
|
||||||
prisma_client
|
Sequence[PrismaDeletedVerificationToken]
|
||||||
).table.find_many(
|
] = await DeletedVerificationTokenRepository(prisma_client).table.find_many(
|
||||||
where={"token": {"in": list(missing_keys)}},
|
where={"token": {"in": list(missing_keys)}},
|
||||||
order={"deleted_at": "desc"},
|
order={"deleted_at": "desc"},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,8 @@ router: Final = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
class _ConfigOverrideRow(Protocol):
|
class _ConfigOverrideRow(Protocol):
|
||||||
config_value: str | Mapping[str, object] | None
|
@property
|
||||||
|
def config_value(self) -> str | Mapping[str, object] | None: ...
|
||||||
|
|
||||||
|
|
||||||
class _ConfigOverridesTableClient(Protocol):
|
class _ConfigOverridesTableClient(Protocol):
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,9 @@ These are members of a Team on LiteLLM
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import traceback
|
import traceback
|
||||||
from collections.abc import Mapping, Sequence
|
from collections.abc import Awaitable, Mapping, Sequence
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Any, Final, Literal, Protocol, cast
|
from typing import Any, Final, Literal, cast
|
||||||
|
|
||||||
import fastapi
|
import fastapi
|
||||||
from fastapi import APIRouter, Depends, Header, HTTPException, Request, status
|
from fastapi import APIRouter, Depends, Header, HTTPException, Request, status
|
||||||
|
|
@ -58,6 +58,7 @@ from litellm.proxy.management_helpers.object_permission_utils import (
|
||||||
from litellm.proxy.management_helpers.utils import management_endpoint_wrapper
|
from litellm.proxy.management_helpers.utils import management_endpoint_wrapper
|
||||||
from litellm.proxy.utils import handle_exception_on_proxy, hash_password
|
from litellm.proxy.utils import handle_exception_on_proxy, hash_password
|
||||||
from litellm.repositories.organization_repository import OrganizationRepository
|
from litellm.repositories.organization_repository import OrganizationRepository
|
||||||
|
from litellm.repositories.prisma_protocols import TableActions
|
||||||
from litellm.repositories.table_repositories import (
|
from litellm.repositories.table_repositories import (
|
||||||
InvitationLinkRepository,
|
InvitationLinkRepository,
|
||||||
OrganizationMembershipRepository,
|
OrganizationMembershipRepository,
|
||||||
|
|
@ -86,15 +87,6 @@ from litellm.types.proxy.management_endpoints.scim_v2 import (
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from prisma import models as prisma_models
|
from prisma import models as prisma_models
|
||||||
from prisma import types as prisma_types
|
from prisma import types as prisma_types
|
||||||
from prisma.actions import (
|
|
||||||
LiteLLM_InvitationLinkActions,
|
|
||||||
LiteLLM_OrganizationMembershipActions,
|
|
||||||
LiteLLM_OrganizationTableActions,
|
|
||||||
LiteLLM_TeamMembershipActions,
|
|
||||||
LiteLLM_TeamTableActions,
|
|
||||||
LiteLLM_UserTableActions,
|
|
||||||
LiteLLM_VerificationTokenActions,
|
|
||||||
)
|
|
||||||
|
|
||||||
from litellm.proxy.common_utils.user_api_key_cache import UserApiKeyCache
|
from litellm.proxy.common_utils.user_api_key_cache import UserApiKeyCache
|
||||||
from litellm.proxy.proxy_server import PrismaClient
|
from litellm.proxy.proxy_server import PrismaClient
|
||||||
|
|
@ -105,31 +97,31 @@ router: Final = APIRouter()
|
||||||
|
|
||||||
def _user_table(
|
def _user_table(
|
||||||
prisma_client: "PrismaClient | None",
|
prisma_client: "PrismaClient | None",
|
||||||
) -> "LiteLLM_UserTableActions[prisma_models.LiteLLM_UserTable]":
|
) -> "TableActions[prisma_models.LiteLLM_UserTable]":
|
||||||
user_table: Final[LiteLLM_UserTableActions[prisma_models.LiteLLM_UserTable]] = UserRepository(prisma_client).table
|
user_table: Final[TableActions[prisma_models.LiteLLM_UserTable]] = UserRepository(prisma_client).table
|
||||||
return user_table
|
return user_table
|
||||||
|
|
||||||
|
|
||||||
def _team_table(
|
def _team_table(
|
||||||
prisma_client: "PrismaClient | None",
|
prisma_client: "PrismaClient | None",
|
||||||
) -> "LiteLLM_TeamTableActions[prisma_models.LiteLLM_TeamTable]":
|
) -> "TableActions[prisma_models.LiteLLM_TeamTable]":
|
||||||
team_table: Final[LiteLLM_TeamTableActions[prisma_models.LiteLLM_TeamTable]] = TeamRepository(prisma_client).table
|
team_table: Final[TableActions[prisma_models.LiteLLM_TeamTable]] = TeamRepository(prisma_client).table
|
||||||
return team_table
|
return team_table
|
||||||
|
|
||||||
|
|
||||||
def _verification_token_table(
|
def _verification_token_table(
|
||||||
prisma_client: "PrismaClient | None",
|
prisma_client: "PrismaClient | None",
|
||||||
) -> "LiteLLM_VerificationTokenActions[prisma_models.LiteLLM_VerificationToken]":
|
) -> "TableActions[prisma_models.LiteLLM_VerificationToken]":
|
||||||
token_table: Final[LiteLLM_VerificationTokenActions[prisma_models.LiteLLM_VerificationToken]] = (
|
token_table: Final[TableActions[prisma_models.LiteLLM_VerificationToken]] = VerificationTokenRepository(
|
||||||
VerificationTokenRepository(prisma_client).table
|
prisma_client
|
||||||
)
|
).table
|
||||||
return token_table
|
return token_table
|
||||||
|
|
||||||
|
|
||||||
def _organization_membership_table(
|
def _organization_membership_table(
|
||||||
prisma_client: "PrismaClient | None",
|
prisma_client: "PrismaClient | None",
|
||||||
) -> "LiteLLM_OrganizationMembershipActions[prisma_models.LiteLLM_OrganizationMembership]":
|
) -> "TableActions[prisma_models.LiteLLM_OrganizationMembership]":
|
||||||
membership_table: Final[LiteLLM_OrganizationMembershipActions[prisma_models.LiteLLM_OrganizationMembership]] = (
|
membership_table: Final[TableActions[prisma_models.LiteLLM_OrganizationMembership]] = (
|
||||||
OrganizationMembershipRepository(prisma_client).table
|
OrganizationMembershipRepository(prisma_client).table
|
||||||
)
|
)
|
||||||
return membership_table
|
return membership_table
|
||||||
|
|
@ -137,8 +129,8 @@ def _organization_membership_table(
|
||||||
|
|
||||||
def _invitation_link_table(
|
def _invitation_link_table(
|
||||||
prisma_client: "PrismaClient | None",
|
prisma_client: "PrismaClient | None",
|
||||||
) -> "LiteLLM_InvitationLinkActions[prisma_models.LiteLLM_InvitationLink]":
|
) -> "TableActions[prisma_models.LiteLLM_InvitationLink]":
|
||||||
invitation_table: LiteLLM_InvitationLinkActions[prisma_models.LiteLLM_InvitationLink] = InvitationLinkRepository(
|
invitation_table: Final[TableActions[prisma_models.LiteLLM_InvitationLink]] = InvitationLinkRepository(
|
||||||
prisma_client
|
prisma_client
|
||||||
).table
|
).table
|
||||||
return invitation_table
|
return invitation_table
|
||||||
|
|
@ -146,19 +138,19 @@ def _invitation_link_table(
|
||||||
|
|
||||||
def _organization_table(
|
def _organization_table(
|
||||||
prisma_client: "PrismaClient | None",
|
prisma_client: "PrismaClient | None",
|
||||||
) -> "LiteLLM_OrganizationTableActions[prisma_models.LiteLLM_OrganizationTable]":
|
) -> "TableActions[prisma_models.LiteLLM_OrganizationTable]":
|
||||||
organization_table: Final[LiteLLM_OrganizationTableActions[prisma_models.LiteLLM_OrganizationTable]] = (
|
organization_table: Final[TableActions[prisma_models.LiteLLM_OrganizationTable]] = OrganizationRepository(
|
||||||
OrganizationRepository(prisma_client).table
|
prisma_client
|
||||||
)
|
).table
|
||||||
return organization_table
|
return organization_table
|
||||||
|
|
||||||
|
|
||||||
def _team_membership_table(
|
def _team_membership_table(
|
||||||
prisma_client: "PrismaClient | None",
|
prisma_client: "PrismaClient | None",
|
||||||
) -> "LiteLLM_TeamMembershipActions[prisma_models.LiteLLM_TeamMembership]":
|
) -> "TableActions[prisma_models.LiteLLM_TeamMembership]":
|
||||||
team_membership_table: Final[LiteLLM_TeamMembershipActions[prisma_models.LiteLLM_TeamMembership]] = (
|
team_membership_table: Final[TableActions[prisma_models.LiteLLM_TeamMembership]] = TeamMembershipRepository(
|
||||||
TeamMembershipRepository(prisma_client).table
|
prisma_client
|
||||||
)
|
).table
|
||||||
return team_membership_table
|
return team_membership_table
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -294,7 +286,7 @@ async def _add_user_to_organizations(
|
||||||
organization_member_add,
|
organization_member_add,
|
||||||
)
|
)
|
||||||
|
|
||||||
tasks: Final = []
|
tasks: Final[list[Awaitable[object]]] = []
|
||||||
for organization_id in organizations:
|
for organization_id in organizations:
|
||||||
tasks.append(
|
tasks.append(
|
||||||
organization_member_add(
|
organization_member_add(
|
||||||
|
|
@ -406,7 +398,7 @@ async def add_new_user_to_default_team(
|
||||||
teams: list[str] | list[NewUserRequestTeam],
|
teams: list[str] | list[NewUserRequestTeam],
|
||||||
prisma_client: "PrismaClient",
|
prisma_client: "PrismaClient",
|
||||||
):
|
):
|
||||||
tasks: Final = []
|
tasks: Final[list[Awaitable[object]]] = []
|
||||||
for team in teams:
|
for team in teams:
|
||||||
user_role: Literal["user", "admin"] = "user"
|
user_role: Literal["user", "admin"] = "user"
|
||||||
max_budget_in_team: float | None = None
|
max_budget_in_team: float | None = None
|
||||||
|
|
@ -1479,7 +1471,8 @@ async def _update_single_user_helper(
|
||||||
# Create new user if not found
|
# Create new user if not found
|
||||||
non_default_values["user_id"] = str(uuid.uuid4())
|
non_default_values["user_id"] = str(uuid.uuid4())
|
||||||
non_default_values["user_email"] = user_request.user_email
|
non_default_values["user_email"] = user_request.user_email
|
||||||
response = await prisma_client.insert_data(data=non_default_values, table_name="user")
|
inserted_user_row: Final = await prisma_client.insert_data(data=non_default_values, table_name="user")
|
||||||
|
response = inserted_user_row # pyright: ignore[reportAssignmentType] # insert_data returns a prisma row
|
||||||
|
|
||||||
if response is not None:
|
if response is not None:
|
||||||
await _schedule_user_update_audit_log(
|
await _schedule_user_update_audit_log(
|
||||||
|
|
@ -1795,7 +1788,9 @@ async def bulk_user_update(
|
||||||
|
|
||||||
# Apply update transformations (reuse existing logic)
|
# Apply update transformations (reuse existing logic)
|
||||||
data_json: Final[dict] = data.user_updates.model_dump(exclude_unset=True)
|
data_json: Final[dict] = data.user_updates.model_dump(exclude_unset=True)
|
||||||
non_default_values: Final = _update_internal_user_params(data_json=data_json, data=data.user_updates)
|
non_default_values: Final[dict[str, object]] = _update_internal_user_params(
|
||||||
|
data_json=data_json, data=data.user_updates
|
||||||
|
)
|
||||||
|
|
||||||
# Remove user identification fields since we're updating by user_id
|
# Remove user identification fields since we're updating by user_id
|
||||||
non_default_values.pop("user_id", None)
|
non_default_values.pop("user_id", None)
|
||||||
|
|
@ -2149,7 +2144,7 @@ async def get_users(
|
||||||
_validate_sort_params(sort_by, sort_order) if sort_by is not None and isinstance(sort_by, str) else None
|
_validate_sort_params(sort_by, sort_order) if sort_by is not None and isinstance(sort_by, str) else None
|
||||||
)
|
)
|
||||||
|
|
||||||
users: Sequence[prisma_models.LiteLLM_UserTable] | None = await UserRepository(prisma_client).table.find_many(
|
users: Final[Sequence[prisma_models.LiteLLM_UserTable]] = await UserRepository(prisma_client).table.find_many(
|
||||||
where=where_conditions,
|
where=where_conditions,
|
||||||
skip=skip,
|
skip=skip,
|
||||||
take=page_size,
|
take=page_size,
|
||||||
|
|
@ -2160,10 +2155,7 @@ async def get_users(
|
||||||
total_count: Final[int] = await UserRepository(prisma_client).table.count(where=where_conditions)
|
total_count: Final[int] = await UserRepository(prisma_client).table.count(where=where_conditions)
|
||||||
|
|
||||||
# Get key count for each user
|
# Get key count for each user
|
||||||
if users is not None:
|
user_key_counts: Final = await get_user_key_counts(prisma_client, [user.user_id for user in users])
|
||||||
user_key_counts = await get_user_key_counts(prisma_client, [user.user_id for user in users])
|
|
||||||
else:
|
|
||||||
user_key_counts = {}
|
|
||||||
|
|
||||||
verbose_proxy_logger.debug("Total count of users: %s", total_count)
|
verbose_proxy_logger.debug("Total count of users: %s", total_count)
|
||||||
|
|
||||||
|
|
@ -2172,17 +2164,14 @@ async def get_users(
|
||||||
|
|
||||||
# Prepare response
|
# Prepare response
|
||||||
user_list: list[LiteLLM_UserTableWithKeyCount] = []
|
user_list: list[LiteLLM_UserTableWithKeyCount] = []
|
||||||
if users is not None:
|
for user in users:
|
||||||
for user in users:
|
user_dump = user.model_dump()
|
||||||
user_dump = user.model_dump()
|
user_dump["metadata"] = _redact_scim_enterprise_metadata(user_dump.get("metadata"))
|
||||||
user_dump["metadata"] = _redact_scim_enterprise_metadata(user_dump.get("metadata"))
|
user_list.append(
|
||||||
user_list.append(
|
LiteLLM_UserTableWithKeyCount.model_validate(
|
||||||
LiteLLM_UserTableWithKeyCount.model_validate(
|
{**user_dump, "key_count": user_key_counts.get(user.user_id, 0)}
|
||||||
{**user_dump, "key_count": user_key_counts.get(user.user_id, 0)}
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
else:
|
)
|
||||||
user_list = []
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"users": user_list,
|
"users": user_list,
|
||||||
|
|
@ -2193,13 +2182,6 @@ async def get_users(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class _DeleteTeamRow(Protocol):
|
|
||||||
team_id: str
|
|
||||||
members_with_roles: object
|
|
||||||
|
|
||||||
def model_dump(self) -> Mapping[str, object]: ...
|
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/user/delete",
|
"/user/delete",
|
||||||
tags=["Internal User management"],
|
tags=["Internal User management"],
|
||||||
|
|
@ -2258,9 +2240,9 @@ async def delete_user(
|
||||||
# loop an org-admin of org-A could delete users in org-B by supplying
|
# loop an org-admin of org-A could delete users in org-B by supplying
|
||||||
# {"user_ids": [victim_in_org_B], "organization_id": "org-A"}.
|
# {"user_ids": [victim_in_org_B], "organization_id": "org-A"}.
|
||||||
caller_is_proxy_admin: Final = user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value
|
caller_is_proxy_admin: Final = user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value
|
||||||
caller_admin_org_ids: set = set()
|
caller_admin_org_ids: set[str] = set()
|
||||||
if not caller_is_proxy_admin:
|
if not caller_is_proxy_admin:
|
||||||
caller_memberships: Final = (
|
caller_memberships: Final[Sequence[prisma_models.LiteLLM_OrganizationMembership]] = (
|
||||||
await _organization_membership_table(prisma_client).find_many(
|
await _organization_membership_table(prisma_client).find_many(
|
||||||
where={
|
where={
|
||||||
"user_id": user_api_key_dict.user_id,
|
"user_id": user_api_key_dict.user_id,
|
||||||
|
|
@ -2279,7 +2261,7 @@ async def delete_user(
|
||||||
|
|
||||||
# Batch-fetch target memberships once before the per-user loop. Avoids
|
# Batch-fetch target memberships once before the per-user loop. Avoids
|
||||||
# an N+1 DB call when delete_user is called with a large user_ids list.
|
# an N+1 DB call when delete_user is called with a large user_ids list.
|
||||||
target_org_ids_by_user: Final[dict[str, set]] = {}
|
target_org_ids_by_user: Final[dict[str, set[str]]] = {}
|
||||||
if not caller_is_proxy_admin:
|
if not caller_is_proxy_admin:
|
||||||
all_target_memberships: Final = await _organization_membership_table(prisma_client).find_many(
|
all_target_memberships: Final = await _organization_membership_table(prisma_client).find_many(
|
||||||
where={"user_id": {"in": data.user_ids}}
|
where={"user_id": {"in": data.user_ids}}
|
||||||
|
|
@ -2319,7 +2301,7 @@ async def delete_user(
|
||||||
# we do this after the first for loop, since first for loop is for validation. we only want this inserted after validation passes
|
# we do this after the first for loop, since first for loop is for validation. we only want this inserted after validation passes
|
||||||
if is_audit_logging_enabled():
|
if is_audit_logging_enabled():
|
||||||
# make an audit log for each team deleted
|
# make an audit log for each team deleted
|
||||||
_user_row = user_row.json(exclude_none=True)
|
_user_row = user_row.model_dump_json(exclude_none=True)
|
||||||
|
|
||||||
asyncio.create_task(
|
asyncio.create_task(
|
||||||
create_audit_log_for_update(
|
create_audit_log_for_update(
|
||||||
|
|
@ -2342,10 +2324,10 @@ async def delete_user(
|
||||||
)
|
)
|
||||||
|
|
||||||
## CLEANUP MEMBERS_WITH_ROLES
|
## CLEANUP MEMBERS_WITH_ROLES
|
||||||
fetch_all_teams: Sequence[_DeleteTeamRow] = await TeamRepository(prisma_client).table.find_many(
|
fetch_all_teams: Sequence[prisma_models.LiteLLM_TeamTable] = await TeamRepository(
|
||||||
where={"team_id": {"in": user_row.teams}}
|
prisma_client
|
||||||
)
|
).table.find_many(where={"team_id": {"in": user_row.teams}})
|
||||||
teams_to_update = []
|
teams_to_update: list[tuple[str, str]] = []
|
||||||
for team in fetch_all_teams:
|
for team in fetch_all_teams:
|
||||||
removed_team_members, new_team_members = _cleanup_members_with_roles(
|
removed_team_members, new_team_members = _cleanup_members_with_roles(
|
||||||
existing_team_row=LiteLLM_TeamTable.model_validate(team.model_dump()),
|
existing_team_row=LiteLLM_TeamTable.model_validate(team.model_dump()),
|
||||||
|
|
@ -2357,15 +2339,14 @@ async def delete_user(
|
||||||
)
|
)
|
||||||
if removed_team_members:
|
if removed_team_members:
|
||||||
_db_new_team_members: list[dict] = [m.model_dump() for m in new_team_members]
|
_db_new_team_members: list[dict] = [m.model_dump() for m in new_team_members]
|
||||||
team.members_with_roles = json.dumps(_db_new_team_members)
|
teams_to_update.append((team.team_id, json.dumps(_db_new_team_members)))
|
||||||
teams_to_update.append(team)
|
|
||||||
|
|
||||||
## update teams
|
## update teams
|
||||||
|
|
||||||
for team in teams_to_update:
|
for team_id, members_with_roles in teams_to_update:
|
||||||
await TeamRepository(prisma_client).table.update(
|
await TeamRepository(prisma_client).table.update(
|
||||||
where={"team_id": team.team_id},
|
where={"team_id": team_id},
|
||||||
data={"members_with_roles": team.members_with_roles},
|
data={"members_with_roles": members_with_roles},
|
||||||
)
|
)
|
||||||
# End of Audit logging
|
# End of Audit logging
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,9 @@ async def update_jwt_key_mapping(
|
||||||
where={"id": data.id}, data=update_data
|
where={"id": data.id}, data=update_data
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if updated_mapping is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Mapping not found")
|
||||||
|
|
||||||
# Invalidate new cache key if claim fields changed
|
# Invalidate new cache key if claim fields changed
|
||||||
cache_key = f"jwt_key_mapping:{updated_mapping.jwt_claim_name}:{updated_mapping.jwt_claim_value}"
|
cache_key = f"jwt_key_mapping:{updated_mapping.jwt_claim_name}:{updated_mapping.jwt_claim_value}"
|
||||||
await user_api_key_cache.async_delete_cache(cache_key)
|
await user_api_key_cache.async_delete_cache(cache_key)
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,7 @@ from litellm.repositories.budget_repository import BudgetRepository
|
||||||
from litellm.repositories.config_repository import ConfigParam, ConfigRepository
|
from litellm.repositories.config_repository import ConfigParam, ConfigRepository
|
||||||
from litellm.repositories.credentials_repository import CredentialsRepository
|
from litellm.repositories.credentials_repository import CredentialsRepository
|
||||||
from litellm.repositories.model_repository import ModelRepository
|
from litellm.repositories.model_repository import ModelRepository
|
||||||
|
from litellm.repositories.prisma_protocols import TableActions
|
||||||
from litellm.repositories.table_repositories import (
|
from litellm.repositories.table_repositories import (
|
||||||
DeletedVerificationTokenRepository,
|
DeletedVerificationTokenRepository,
|
||||||
DeprecatedVerificationTokenRepository,
|
DeprecatedVerificationTokenRepository,
|
||||||
|
|
@ -151,65 +152,22 @@ from litellm.types.utils import (
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from prisma import Prisma
|
from prisma import Prisma
|
||||||
|
from prisma import models as prisma_models
|
||||||
|
|
||||||
_PrismaRowT = TypeVar("_PrismaRowT")
|
|
||||||
_RepositoryModelT = TypeVar("_RepositoryModelT", bound=BaseModel)
|
_RepositoryModelT = TypeVar("_RepositoryModelT", bound=BaseModel)
|
||||||
|
|
||||||
|
|
||||||
class _PrismaTableActions(Protocol[_PrismaRowT]):
|
|
||||||
"""Typed view of the Prisma table actions a repository exposes through its untyped ``table``."""
|
|
||||||
|
|
||||||
async def find_unique(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
where: Mapping[str, object],
|
|
||||||
include: Mapping[str, object] | None = None,
|
|
||||||
) -> _PrismaRowT | None: ...
|
|
||||||
|
|
||||||
async def find_first(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
where: Mapping[str, object],
|
|
||||||
include: Mapping[str, object] | None = None,
|
|
||||||
) -> _PrismaRowT | None: ...
|
|
||||||
|
|
||||||
async def find_many(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
where: Mapping[str, object] | None = None,
|
|
||||||
include: Mapping[str, object] | None = None,
|
|
||||||
order: Mapping[str, object] | None = None,
|
|
||||||
skip: int | None = None,
|
|
||||||
take: int | None = None,
|
|
||||||
) -> list[_PrismaRowT]: ...
|
|
||||||
|
|
||||||
async def count(self, *, where: Mapping[str, object] | None = None) -> int: ...
|
|
||||||
|
|
||||||
async def create(self, *, data: Mapping[str, object]) -> _PrismaRowT: ...
|
|
||||||
|
|
||||||
async def create_many(self, *, data: Sequence[Mapping[str, object]]) -> int: ...
|
|
||||||
|
|
||||||
async def delete_many(self, *, where: Mapping[str, object] | None = None) -> int: ...
|
|
||||||
|
|
||||||
async def update(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
where: Mapping[str, object],
|
|
||||||
data: Mapping[str, object],
|
|
||||||
) -> _PrismaRowT | None: ...
|
|
||||||
|
|
||||||
async def upsert(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
where: Mapping[str, object],
|
|
||||||
data: Mapping[str, object],
|
|
||||||
) -> _PrismaRowT: ...
|
|
||||||
|
|
||||||
|
|
||||||
class _UserRowLike(Protocol):
|
class _UserRowLike(Protocol):
|
||||||
user_id: str | None
|
"""Read-only view of the user columns ``/key/list`` expands keys with."""
|
||||||
user_email: str | None
|
|
||||||
user_alias: str | None
|
@property
|
||||||
|
def user_id(self) -> str | None: ...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def user_email(self) -> str | None: ...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def user_alias(self) -> str | None: ...
|
||||||
|
|
||||||
def model_dump(self) -> Mapping[str, object]: ...
|
def model_dump(self) -> Mapping[str, object]: ...
|
||||||
|
|
||||||
|
|
@ -217,46 +175,56 @@ class _UserRowLike(Protocol):
|
||||||
|
|
||||||
|
|
||||||
class _TxTables(Protocol):
|
class _TxTables(Protocol):
|
||||||
litellm_proxymodeltable: _PrismaTableActions[object]
|
litellm_proxymodeltable: TableActions[object]
|
||||||
|
|
||||||
|
|
||||||
class _TableSource(Protocol[_PrismaRowT]):
|
class _ConfigTableActions(Protocol):
|
||||||
"""Repository view that exposes its untyped Prisma ``table`` with a concrete row type."""
|
"""Config table surface this module needs; the shared repository seam exposes no ``update``."""
|
||||||
|
|
||||||
@property
|
async def find_many(self) -> Sequence[ConfigParam]: ...
|
||||||
def table(self) -> _PrismaTableActions[_PrismaRowT]: ...
|
|
||||||
|
|
||||||
|
async def update(
|
||||||
def _table_of(source: _TableSource[_PrismaRowT]) -> _PrismaTableActions[_PrismaRowT]:
|
self,
|
||||||
return source.table
|
*,
|
||||||
|
where: Mapping[str, object],
|
||||||
|
data: Mapping[str, object],
|
||||||
|
) -> ConfigParam | None: ...
|
||||||
|
|
||||||
|
|
||||||
def _prisma_table(
|
def _prisma_table(
|
||||||
repository: BaseRepository[_RepositoryModelT],
|
repository: BaseRepository[_RepositoryModelT],
|
||||||
) -> _PrismaTableActions[_RepositoryModelT]:
|
) -> TableActions[_RepositoryModelT]:
|
||||||
return _table_of(repository)
|
return cast( # cast-ok: callers read only the field names the prisma row and repository model share
|
||||||
|
"TableActions[_RepositoryModelT]", repository.table
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _deleted_verification_token_table(
|
def _deleted_verification_token_table(
|
||||||
prisma_client: PrismaClient,
|
prisma_client: PrismaClient,
|
||||||
) -> _PrismaTableActions[LiteLLM_DeletedVerificationToken]:
|
) -> "TableActions[prisma_models.LiteLLM_DeletedVerificationToken]":
|
||||||
return _table_of(DeletedVerificationTokenRepository(prisma_client))
|
return DeletedVerificationTokenRepository(prisma_client).table
|
||||||
|
|
||||||
|
|
||||||
def _deprecated_verification_token_table(prisma_client: PrismaClient) -> _PrismaTableActions[object]:
|
def _deprecated_verification_token_table(
|
||||||
return _table_of(DeprecatedVerificationTokenRepository(prisma_client))
|
prisma_client: PrismaClient,
|
||||||
|
) -> "TableActions[prisma_models.LiteLLM_DeprecatedVerificationToken]":
|
||||||
|
return DeprecatedVerificationTokenRepository(prisma_client).table
|
||||||
|
|
||||||
|
|
||||||
def _user_table(prisma_client: PrismaClient) -> _PrismaTableActions[_UserRowLike]:
|
def _user_table(prisma_client: PrismaClient) -> TableActions[_UserRowLike]:
|
||||||
return _table_of(UserRepository(prisma_client))
|
return UserRepository(prisma_client).table
|
||||||
|
|
||||||
|
|
||||||
def _credentials_table(prisma_client: PrismaClient) -> _PrismaTableActions[CredentialItem]:
|
def _credentials_table(prisma_client: PrismaClient) -> TableActions[CredentialItem]:
|
||||||
return _table_of(CredentialsRepository(prisma_client))
|
return cast( # cast-ok: the rotation loop reads and rewrites these rows through CredentialItem names only
|
||||||
|
"TableActions[CredentialItem]", CredentialsRepository(prisma_client).table
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _config_table(prisma_client: PrismaClient) -> _PrismaTableActions[ConfigParam]:
|
def _config_table(prisma_client: PrismaClient) -> _ConfigTableActions:
|
||||||
return _table_of(ConfigRepository(prisma_client))
|
return cast( # cast-ok: ConfigRepository.table hides the write actions this module needs on that same object
|
||||||
|
"_ConfigTableActions", ConfigRepository(prisma_client).table
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def _check_custom_key_allowed(custom_key_value: str | None) -> None:
|
async def _check_custom_key_allowed(custom_key_value: str | None) -> None:
|
||||||
|
|
@ -1046,7 +1014,7 @@ async def _common_key_generation_helper(
|
||||||
)
|
)
|
||||||
new_budget: Final = prisma_client.jsonify_object(budget_row.json(exclude_none=True))
|
new_budget: Final = prisma_client.jsonify_object(budget_row.json(exclude_none=True))
|
||||||
|
|
||||||
_budget: Final[LiteLLM_BudgetTable] = await BudgetRepository(prisma_client).table.create(
|
_budget: Final[prisma_models.LiteLLM_BudgetTable] = await BudgetRepository(prisma_client).table.create(
|
||||||
data={
|
data={
|
||||||
**new_budget,
|
**new_budget,
|
||||||
"created_by": user_api_key_dict.user_id or litellm_proxy_admin_name,
|
"created_by": user_api_key_dict.user_id or litellm_proxy_admin_name,
|
||||||
|
|
@ -1252,7 +1220,7 @@ async def _common_key_generation_helper(
|
||||||
|
|
||||||
|
|
||||||
def _check_key_model_specific_limits(
|
def _check_key_model_specific_limits(
|
||||||
keys: list[LiteLLM_VerificationToken],
|
keys: Sequence[LiteLLM_VerificationToken],
|
||||||
data: GenerateKeyRequest | UpdateKeyRequest,
|
data: GenerateKeyRequest | UpdateKeyRequest,
|
||||||
entity_rpm_limit: int | None,
|
entity_rpm_limit: int | None,
|
||||||
entity_tpm_limit: int | None,
|
entity_tpm_limit: int | None,
|
||||||
|
|
@ -1323,7 +1291,7 @@ def _check_key_model_specific_limits(
|
||||||
|
|
||||||
|
|
||||||
def _check_key_rpm_tpm_limits(
|
def _check_key_rpm_tpm_limits(
|
||||||
keys: list[LiteLLM_VerificationToken],
|
keys: Sequence[LiteLLM_VerificationToken],
|
||||||
data: GenerateKeyRequest | UpdateKeyRequest,
|
data: GenerateKeyRequest | UpdateKeyRequest,
|
||||||
entity_rpm_limit: int | None,
|
entity_rpm_limit: int | None,
|
||||||
entity_tpm_limit: int | None,
|
entity_tpm_limit: int | None,
|
||||||
|
|
@ -1361,7 +1329,7 @@ def _check_key_rpm_tpm_limits(
|
||||||
|
|
||||||
|
|
||||||
def check_team_key_model_specific_limits(
|
def check_team_key_model_specific_limits(
|
||||||
keys: list[LiteLLM_VerificationToken],
|
keys: Sequence[LiteLLM_VerificationToken],
|
||||||
team_table: LiteLLM_TeamTableCachedObj,
|
team_table: LiteLLM_TeamTableCachedObj,
|
||||||
data: GenerateKeyRequest | UpdateKeyRequest,
|
data: GenerateKeyRequest | UpdateKeyRequest,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
@ -1386,7 +1354,7 @@ def check_team_key_model_specific_limits(
|
||||||
|
|
||||||
|
|
||||||
def check_team_key_rpm_tpm_limits(
|
def check_team_key_rpm_tpm_limits(
|
||||||
keys: list[LiteLLM_VerificationToken],
|
keys: Sequence[LiteLLM_VerificationToken],
|
||||||
team_table: LiteLLM_TeamTableCachedObj,
|
team_table: LiteLLM_TeamTableCachedObj,
|
||||||
data: GenerateKeyRequest | UpdateKeyRequest,
|
data: GenerateKeyRequest | UpdateKeyRequest,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
@ -1494,7 +1462,7 @@ async def _check_project_key_limits(
|
||||||
|
|
||||||
|
|
||||||
def check_org_key_model_specific_limits(
|
def check_org_key_model_specific_limits(
|
||||||
keys: list[LiteLLM_VerificationToken],
|
keys: Sequence[LiteLLM_VerificationToken],
|
||||||
org_table: LiteLLM_OrganizationTable,
|
org_table: LiteLLM_OrganizationTable,
|
||||||
data: GenerateKeyRequest | UpdateKeyRequest,
|
data: GenerateKeyRequest | UpdateKeyRequest,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
@ -1527,7 +1495,7 @@ def check_org_key_model_specific_limits(
|
||||||
|
|
||||||
|
|
||||||
def check_org_key_rpm_tpm_limits(
|
def check_org_key_rpm_tpm_limits(
|
||||||
keys: list[LiteLLM_VerificationToken],
|
keys: Sequence[LiteLLM_VerificationToken],
|
||||||
org_table: LiteLLM_OrganizationTable,
|
org_table: LiteLLM_OrganizationTable,
|
||||||
data: GenerateKeyRequest | UpdateKeyRequest,
|
data: GenerateKeyRequest | UpdateKeyRequest,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
@ -2242,9 +2210,9 @@ async def _get_and_validate_existing_key(
|
||||||
code=status.HTTP_400_BAD_REQUEST,
|
code=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
rows: list[LiteLLM_VerificationToken] = await _prisma_table(VerificationTokenRepository(prisma_client)).find_many(
|
rows: Sequence[LiteLLM_VerificationToken] = await _prisma_table(
|
||||||
where={"key_alias": key_alias}, take=2
|
VerificationTokenRepository(prisma_client)
|
||||||
)
|
).find_many(where={"key_alias": key_alias}, take=2)
|
||||||
|
|
||||||
if len(rows) == 0:
|
if len(rows) == 0:
|
||||||
raise ProxyException(
|
raise ProxyException(
|
||||||
|
|
@ -2407,7 +2375,10 @@ async def _process_single_key_update(
|
||||||
)
|
)
|
||||||
|
|
||||||
_data: Final = {**non_default_values, "token": update_key_request.key}
|
_data: Final = {**non_default_values, "token": update_key_request.key}
|
||||||
response: Final = await prisma_client.update_data(token=update_key_request.key, data=_data)
|
response: Final[Mapping[str, object] | None] = cast( # cast-ok: every update_data branch returns a str-keyed dict
|
||||||
|
"Mapping[str, object] | None",
|
||||||
|
await prisma_client.update_data(token=update_key_request.key, data=_data),
|
||||||
|
)
|
||||||
|
|
||||||
# Delete cache
|
# Delete cache
|
||||||
await _delete_cache_key_object(
|
await _delete_cache_key_object(
|
||||||
|
|
@ -3225,7 +3196,7 @@ async def bulk_update_team_keys(
|
||||||
# `blocked` is Boolean? with no default; `/key/generate` writes NULL. Prisma's `NOT`
|
# `blocked` is Boolean? with no default; `/key/generate` writes NULL. Prisma's `NOT`
|
||||||
# excludes NULLs, so explicitly OR `false` with `null` to include them.
|
# excludes NULLs, so explicitly OR `false` with `null` to include them.
|
||||||
now: Final = datetime.now(timezone.utc)
|
now: Final = datetime.now(timezone.utc)
|
||||||
existing_keys = await VerificationTokenRepository(prisma_client).table.find_many(
|
existing_keys = await _prisma_table(VerificationTokenRepository(prisma_client)).find_many(
|
||||||
where={
|
where={
|
||||||
"team_id": data.team_id,
|
"team_id": data.team_id,
|
||||||
"AND": [
|
"AND": [
|
||||||
|
|
@ -3243,7 +3214,9 @@ async def bulk_update_team_keys(
|
||||||
"error": f"Team {data.team_id} has more than {MAX_BATCH_SIZE} keys. Use `key_ids` to update in batches of {MAX_BATCH_SIZE}."
|
"error": f"Team {data.team_id} has more than {MAX_BATCH_SIZE} keys. Use `key_ids` to update in batches of {MAX_BATCH_SIZE}."
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
requested_tokens = [row.token for row in existing_keys]
|
requested_tokens = cast( # cast-ok: token is the table's primary key, so a row read back always carries one
|
||||||
|
"list[str]", [row.token for row in existing_keys]
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
if data.key_ids is None or len(data.key_ids) == 0:
|
if data.key_ids is None or len(data.key_ids) == 0:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
@ -3261,7 +3234,7 @@ async def bulk_update_team_keys(
|
||||||
seen_hashes.add(h)
|
seen_hashes.add(h)
|
||||||
requested_tokens.append(k)
|
requested_tokens.append(k)
|
||||||
hashed_key_ids.append(h)
|
hashed_key_ids.append(h)
|
||||||
existing_keys = await VerificationTokenRepository(prisma_client).table.find_many(
|
existing_keys = await _prisma_table(VerificationTokenRepository(prisma_client)).find_many(
|
||||||
where={"team_id": data.team_id, "token": {"in": hashed_key_ids}}
|
where={"team_id": data.team_id, "token": {"in": hashed_key_ids}}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -3698,7 +3671,7 @@ async def info_key_fn(
|
||||||
hashed_key: str | None = key
|
hashed_key: str | None = key
|
||||||
if key is not None:
|
if key is not None:
|
||||||
hashed_key = _hash_token_if_needed(token=key)
|
hashed_key = _hash_token_if_needed(token=key)
|
||||||
key_info = await VerificationTokenRepository(prisma_client).table.find_unique(
|
key_info = await _prisma_table(VerificationTokenRepository(prisma_client)).find_unique(
|
||||||
where={"token": hashed_key},
|
where={"token": hashed_key},
|
||||||
include={"litellm_budget_table": True},
|
include={"litellm_budget_table": True},
|
||||||
)
|
)
|
||||||
|
|
@ -3727,7 +3700,7 @@ async def info_key_fn(
|
||||||
key_info = key_info.model_dump()
|
key_info = key_info.model_dump()
|
||||||
except Exception:
|
except Exception:
|
||||||
# if using pydantic v1
|
# if using pydantic v1
|
||||||
key_info = key_info.dict()
|
key_info = key_info.dict() # pyright: ignore[reportDeprecated] # deliberate pydantic v1 fallback
|
||||||
key_token_hash: Final = key_info.pop("token")
|
key_token_hash: Final = key_info.pop("token")
|
||||||
|
|
||||||
model_max_budget = key_info.get("model_max_budget") or {}
|
model_max_budget = key_info.get("model_max_budget") or {}
|
||||||
|
|
@ -4012,7 +3985,10 @@ async def generate_key_helper_fn(
|
||||||
if table_name is None or table_name == "user": # do not auto-create users for `/key/generate`
|
if table_name is None or table_name == "user": # do not auto-create users for `/key/generate`
|
||||||
## CREATE USER (If necessary)
|
## CREATE USER (If necessary)
|
||||||
if query_type == "insert_data":
|
if query_type == "insert_data":
|
||||||
user_row = await prisma_client.insert_data(data=user_data, table_name="user")
|
user_row = cast( # cast-ok: table_name="user" is the insert_data branch returning the user row
|
||||||
|
"prisma_models.LiteLLM_UserTable | None",
|
||||||
|
await prisma_client.insert_data(data=user_data, table_name="user"),
|
||||||
|
)
|
||||||
|
|
||||||
if user_row is None:
|
if user_row is None:
|
||||||
raise Exception("Failed to create user")
|
raise Exception("Failed to create user")
|
||||||
|
|
@ -4219,9 +4195,12 @@ async def delete_verification_tokens(
|
||||||
if prisma_client:
|
if prisma_client:
|
||||||
hashed_tokens: Final[list[str]] = [_hash_token_if_needed(token=key) for key in tokens]
|
hashed_tokens: Final[list[str]] = [_hash_token_if_needed(token=key) for key in tokens]
|
||||||
tokens = hashed_tokens
|
tokens = hashed_tokens
|
||||||
_keys_being_deleted: Final[list[LiteLLM_VerificationToken]] = await _prisma_table(
|
_keys_being_deleted: Final[list[LiteLLM_VerificationToken]] = cast( # cast-ok: find_many returns a list
|
||||||
VerificationTokenRepository(prisma_client)
|
"list[LiteLLM_VerificationToken]",
|
||||||
).find_many(where={"token": {"in": hashed_tokens}})
|
await _prisma_table(VerificationTokenRepository(prisma_client)).find_many(
|
||||||
|
where={"token": {"in": hashed_tokens}}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
if len(_keys_being_deleted) == 0:
|
if len(_keys_being_deleted) == 0:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
@ -4297,7 +4276,7 @@ async def delete_verification_tokens(
|
||||||
|
|
||||||
|
|
||||||
def _transform_verification_tokens_to_deleted_records(
|
def _transform_verification_tokens_to_deleted_records(
|
||||||
keys: list[LiteLLM_VerificationToken],
|
keys: Sequence[LiteLLM_VerificationToken],
|
||||||
user_api_key_dict: UserAPIKeyAuth,
|
user_api_key_dict: UserAPIKeyAuth,
|
||||||
litellm_changed_by: str | None = None,
|
litellm_changed_by: str | None = None,
|
||||||
) -> list[dict[str, object]]:
|
) -> list[dict[str, object]]:
|
||||||
|
|
@ -4372,7 +4351,7 @@ async def _save_deleted_verification_token_records(
|
||||||
|
|
||||||
|
|
||||||
async def _persist_deleted_verification_tokens(
|
async def _persist_deleted_verification_tokens(
|
||||||
keys: list[LiteLLM_VerificationToken],
|
keys: Sequence[LiteLLM_VerificationToken],
|
||||||
prisma_client: PrismaClient,
|
prisma_client: PrismaClient,
|
||||||
user_api_key_dict: UserAPIKeyAuth,
|
user_api_key_dict: UserAPIKeyAuth,
|
||||||
litellm_changed_by: str | None = None,
|
litellm_changed_by: str | None = None,
|
||||||
|
|
@ -4435,7 +4414,9 @@ async def _rotate_master_key(
|
||||||
from litellm.proxy.proxy_server import proxy_config
|
from litellm.proxy.proxy_server import proxy_config
|
||||||
|
|
||||||
try:
|
try:
|
||||||
models: list | None = await _prisma_table(ModelRepository(prisma_client)).find_many()
|
models: list | None = cast( # cast-ok: find_many returns a real list, which TableActions widens to Sequence
|
||||||
|
"list[object]", await _prisma_table(ModelRepository(prisma_client)).find_many()
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
models = None
|
models = None
|
||||||
# 2. process model table
|
# 2. process model table
|
||||||
|
|
@ -5361,9 +5342,9 @@ async def validate_key_list_check(
|
||||||
|
|
||||||
if key_hash:
|
if key_hash:
|
||||||
try:
|
try:
|
||||||
key_info: Final[LiteLLM_VerificationToken] = await VerificationTokenRepository(
|
key_info: Final[LiteLLM_VerificationToken | None] = await _prisma_table(
|
||||||
prisma_client
|
VerificationTokenRepository(prisma_client)
|
||||||
).table.find_unique(
|
).find_unique(
|
||||||
where={"token": key_hash},
|
where={"token": key_hash},
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|
@ -5373,6 +5354,13 @@ async def validate_key_list_check(
|
||||||
param="key_hash",
|
param="key_hash",
|
||||||
code=status.HTTP_403_FORBIDDEN,
|
code=status.HTTP_403_FORBIDDEN,
|
||||||
)
|
)
|
||||||
|
if key_info is None:
|
||||||
|
raise ProxyException(
|
||||||
|
message="Key Hash not found.",
|
||||||
|
type=ProxyErrorTypes.bad_request_error,
|
||||||
|
param="key_hash",
|
||||||
|
code=status.HTTP_403_FORBIDDEN,
|
||||||
|
)
|
||||||
can_user_query_key_info: Final = await _can_user_query_key_info(
|
can_user_query_key_info: Final = await _can_user_query_key_info(
|
||||||
user_api_key_dict=user_api_key_dict,
|
user_api_key_dict=user_api_key_dict,
|
||||||
key=key_hash,
|
key=key_hash,
|
||||||
|
|
@ -5394,8 +5382,9 @@ async def _fetch_user_team_objects(
|
||||||
if complete_user_info is None or not complete_user_info.teams:
|
if complete_user_info is None or not complete_user_info.teams:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
teams: Final[list[BaseModel] | None] = await TeamRepository(prisma_client).table.find_many(
|
teams: Final[Sequence[BaseModel] | None] = cast( # cast-ok: the None guard below predates the non-optional seam
|
||||||
where={"team_id": {"in": complete_user_info.teams}}
|
"Sequence[BaseModel] | None",
|
||||||
|
await TeamRepository(prisma_client).table.find_many(where={"team_id": {"in": complete_user_info.teams}}),
|
||||||
)
|
)
|
||||||
if teams is None:
|
if teams is None:
|
||||||
return []
|
return []
|
||||||
|
|
@ -6130,7 +6119,7 @@ async def _list_key_helper(
|
||||||
key_dict = key.model_dump()
|
key_dict = key.model_dump()
|
||||||
except Exception:
|
except Exception:
|
||||||
# Fallback for Pydantic v1 compatibility
|
# Fallback for Pydantic v1 compatibility
|
||||||
key_dict = key.dict()
|
key_dict = key.dict() # pyright: ignore[reportDeprecated] # deliberate pydantic v1 fallback
|
||||||
# Attach object_permission if object_permission_id is set (only for non-deleted keys)
|
# Attach object_permission if object_permission_id is set (only for non-deleted keys)
|
||||||
if not use_deleted_table:
|
if not use_deleted_table:
|
||||||
key_dict = await attach_object_permission_to_dict(key_dict, prisma_client)
|
key_dict = await attach_object_permission_to_dict(key_dict, prisma_client)
|
||||||
|
|
@ -6155,7 +6144,9 @@ async def _list_key_helper(
|
||||||
# Use deleted key type to preserve deleted_at, deleted_by, etc.
|
# Use deleted key type to preserve deleted_at, deleted_by, etc.
|
||||||
key_list.append(LiteLLM_DeletedVerificationToken.model_validate(key_dict))
|
key_list.append(LiteLLM_DeletedVerificationToken.model_validate(key_dict))
|
||||||
else:
|
else:
|
||||||
key_list.append(UserAPIKeyAuth(**key_dict)) # Return full key object
|
key_list.append(
|
||||||
|
UserAPIKeyAuth(**key_dict) # pyright: ignore[reportAny] # model_dump() is dict[str, Any]
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
_token = key_dict.get("token")
|
_token = key_dict.get("token")
|
||||||
key_list.append(cast(str, _token)) # Return only the token
|
key_list.append(cast(str, _token)) # Return only the token
|
||||||
|
|
|
||||||
|
|
@ -40,17 +40,22 @@ router: Final = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
class _DeploymentRow(Protocol):
|
class _DeploymentRow(Protocol):
|
||||||
model_id: str
|
@property
|
||||||
model_name: str
|
def model_id(self) -> str: ...
|
||||||
model_info: object
|
|
||||||
|
@property
|
||||||
|
def model_name(self) -> str: ...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def model_info(self) -> object: ...
|
||||||
|
|
||||||
|
|
||||||
class _ModelTableClient(Protocol):
|
class _ModelTableClient(Protocol):
|
||||||
async def find_many(self, where: Mapping[str, object] | None = None) -> Sequence[_DeploymentRow]: ...
|
async def find_many(self, *, where: Mapping[str, object] | None = None) -> Sequence[_DeploymentRow]: ...
|
||||||
|
|
||||||
async def find_unique(self, where: Mapping[str, object]) -> _DeploymentRow | None: ...
|
async def find_unique(self, *, where: Mapping[str, object]) -> _DeploymentRow | None: ...
|
||||||
|
|
||||||
async def update(self, where: Mapping[str, object], data: Mapping[str, object]) -> object: ...
|
async def update(self, *, where: Mapping[str, object], data: Mapping[str, object]) -> object: ...
|
||||||
|
|
||||||
|
|
||||||
def _model_table(prisma_client: PrismaClient) -> _ModelTableClient:
|
def _model_table(prisma_client: PrismaClient) -> _ModelTableClient:
|
||||||
|
|
@ -322,7 +327,9 @@ async def get_all_access_groups_from_db(
|
||||||
|
|
||||||
for deployment in deployments:
|
for deployment in deployments:
|
||||||
model_info = deployment.model_info or {}
|
model_info = deployment.model_info or {}
|
||||||
access_groups = model_info.get("access_groups", [])
|
access_groups = model_info.get( # pyright: ignore[reportAttributeAccessIssue] # Json reads back as a dict
|
||||||
|
"access_groups", []
|
||||||
|
)
|
||||||
model_name = deployment.model_name
|
model_name = deployment.model_name
|
||||||
|
|
||||||
for access_group in access_groups:
|
for access_group in access_groups:
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import json
|
||||||
from collections.abc import Awaitable, Mapping, Sequence
|
from collections.abc import Awaitable, Mapping, Sequence
|
||||||
from json import JSONDecodeError
|
from json import JSONDecodeError
|
||||||
from types import MappingProxyType
|
from types import MappingProxyType
|
||||||
from typing import Final, Literal, Protocol, cast
|
from typing import TYPE_CHECKING, Final, Literal, Protocol, cast
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Header, HTTPException, Request, status
|
from fastapi import APIRouter, Depends, Header, HTTPException, Request, status
|
||||||
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
||||||
|
|
@ -72,6 +72,7 @@ from litellm.proxy.spend_tracking.ptu_feature_flag import (
|
||||||
)
|
)
|
||||||
from litellm.proxy.utils import PrismaClient, ProxyLogging
|
from litellm.proxy.utils import PrismaClient, ProxyLogging
|
||||||
from litellm.repositories.model_repository import ModelRepository
|
from litellm.repositories.model_repository import ModelRepository
|
||||||
|
from litellm.repositories.prisma_protocols import TableActions
|
||||||
from litellm.repositories.table_repositories import ModelTableRepository
|
from litellm.repositories.table_repositories import ModelTableRepository
|
||||||
from litellm.repositories.team_repository import TeamRepository
|
from litellm.repositories.team_repository import TeamRepository
|
||||||
from litellm.router import Router
|
from litellm.router import Router
|
||||||
|
|
@ -100,6 +101,9 @@ from litellm.types.router import (
|
||||||
)
|
)
|
||||||
from litellm.utils import get_utc_datetime
|
from litellm.utils import get_utc_datetime
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from prisma import models as prisma_models
|
||||||
|
|
||||||
router: Final = APIRouter()
|
router: Final = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -120,10 +124,14 @@ class UpdatePublicModelGroupsRequest(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class _ProxyModelRow(Protocol):
|
class _ProxyModelRow(Protocol):
|
||||||
model_id: str
|
@property
|
||||||
model_name: str
|
def model_id(self) -> str: ...
|
||||||
litellm_params: Mapping[str, object]
|
|
||||||
model_info: Mapping[str, object] | None
|
@property
|
||||||
|
def model_name(self) -> str: ...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def model_info(self) -> object: ...
|
||||||
|
|
||||||
def model_dump_json(self, *, exclude_none: bool = False) -> str: ...
|
def model_dump_json(self, *, exclude_none: bool = False) -> str: ...
|
||||||
|
|
||||||
|
|
@ -133,7 +141,9 @@ class _ProxyModelTable(Protocol):
|
||||||
|
|
||||||
def find_many(self, *, where: Mapping[str, object]) -> Awaitable[Sequence[_ProxyModelRow]]: ...
|
def find_many(self, *, where: Mapping[str, object]) -> Awaitable[Sequence[_ProxyModelRow]]: ...
|
||||||
|
|
||||||
def update(self, *, where: Mapping[str, object], data: Mapping[str, object]) -> Awaitable[_ProxyModelRow]: ...
|
def update(
|
||||||
|
self, *, where: Mapping[str, object], data: Mapping[str, object]
|
||||||
|
) -> Awaitable[_ProxyModelRow | None]: ...
|
||||||
|
|
||||||
def delete(self, *, where: Mapping[str, object]) -> Awaitable[_ProxyModelRow | None]: ...
|
def delete(self, *, where: Mapping[str, object]) -> Awaitable[_ProxyModelRow | None]: ...
|
||||||
|
|
||||||
|
|
@ -144,41 +154,35 @@ class _TxModelTables(Protocol):
|
||||||
litellm_proxymodeltable: _ProxyModelTable
|
litellm_proxymodeltable: _ProxyModelTable
|
||||||
|
|
||||||
|
|
||||||
|
class _ExistingModelRow(Protocol):
|
||||||
|
@property
|
||||||
|
def litellm_params(self) -> Mapping[str, object]: ...
|
||||||
|
|
||||||
|
def model_dump_json(self, *, exclude_none: bool = False) -> str: ...
|
||||||
|
|
||||||
|
|
||||||
class _TeamRow(Protocol):
|
class _TeamRow(Protocol):
|
||||||
models: Sequence[str]
|
@property
|
||||||
|
def models(self) -> Sequence[str]: ...
|
||||||
|
|
||||||
def model_dump(self) -> Mapping[str, object]: ...
|
def model_dump(self) -> Mapping[str, object]: ...
|
||||||
|
|
||||||
|
|
||||||
class _TeamTable(Protocol):
|
class _TeamLookupTable(Protocol):
|
||||||
def find_unique(self, *, where: Mapping[str, object]) -> Awaitable[_TeamRow | None]: ...
|
def find_unique(self, *, where: Mapping[str, object]) -> Awaitable[_TeamRow | None]: ...
|
||||||
|
|
||||||
|
|
||||||
|
class _TeamTable(_TeamLookupTable, Protocol):
|
||||||
def update(
|
def update(
|
||||||
self, *, where: Mapping[str, object], data: Mapping[str, object], include: Mapping[str, bool]
|
self, *, where: Mapping[str, object], data: Mapping[str, object], include: Mapping[str, bool]
|
||||||
) -> Awaitable[LiteLLM_TeamTable]: ...
|
) -> Awaitable[LiteLLM_TeamTable]: ...
|
||||||
|
|
||||||
|
|
||||||
class _TeamIdRef(Protocol):
|
|
||||||
team_id: str
|
|
||||||
|
|
||||||
|
|
||||||
class _ModelAliasRow(Protocol):
|
|
||||||
id: int
|
|
||||||
model_aliases: dict[str, str]
|
|
||||||
team: _TeamIdRef | None
|
|
||||||
|
|
||||||
|
|
||||||
class _ModelAliasTable(Protocol):
|
|
||||||
def find_many(self, *, include: Mapping[str, bool]) -> Awaitable[Sequence[_ModelAliasRow]]: ...
|
|
||||||
|
|
||||||
def update(self, *, where: Mapping[str, object], data: Mapping[str, object]) -> Awaitable[object]: ...
|
|
||||||
|
|
||||||
|
|
||||||
def _proxy_model_table(prisma_client: PrismaClient) -> _ProxyModelTable:
|
def _proxy_model_table(prisma_client: PrismaClient) -> _ProxyModelTable:
|
||||||
return ModelRepository(prisma_client).table
|
return ModelRepository(prisma_client).table
|
||||||
|
|
||||||
|
|
||||||
def _repo_team_table(prisma_client: PrismaClient) -> _TeamTable:
|
def _repo_team_table(prisma_client: PrismaClient) -> _TeamLookupTable:
|
||||||
return TeamRepository(prisma_client).table
|
return TeamRepository(prisma_client).table
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -186,7 +190,7 @@ def _db_team_table(prisma_client: PrismaClient) -> _TeamTable:
|
||||||
return prisma_client.db.litellm_teamtable
|
return prisma_client.db.litellm_teamtable
|
||||||
|
|
||||||
|
|
||||||
def _model_alias_table(prisma_client: PrismaClient) -> _ModelAliasTable:
|
def _model_alias_table(prisma_client: PrismaClient) -> "TableActions[prisma_models.LiteLLM_ModelTable]":
|
||||||
return ModelTableRepository(prisma_client).table
|
return ModelTableRepository(prisma_client).table
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -677,6 +681,14 @@ async def patch_model(
|
||||||
data=update_data,
|
data=update_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if updated_model is None:
|
||||||
|
raise ProxyException(
|
||||||
|
message=f"Model {model_id} not found on proxy.",
|
||||||
|
type=ProxyErrorTypes.not_found_error,
|
||||||
|
code=status.HTTP_404_NOT_FOUND,
|
||||||
|
param=None,
|
||||||
|
)
|
||||||
|
|
||||||
# Clear cache and reload models (uses config setting or defaults to preserving config models for DB updates)
|
# Clear cache and reload models (uses config setting or defaults to preserving config models for DB updates)
|
||||||
live_before_reload: Final = live_model_ids_snapshot()
|
live_before_reload: Final = live_model_ids_snapshot()
|
||||||
reload_outcome: Final = await clear_cache()
|
reload_outcome: Final = await clear_cache()
|
||||||
|
|
@ -811,7 +823,7 @@ async def _set_model_blocked_status(
|
||||||
live_after=reload_outcome.live_after,
|
live_after=reload_outcome.live_after,
|
||||||
)
|
)
|
||||||
|
|
||||||
return updated_model
|
return updated_model # pyright: ignore[reportReturnType] # prisma row, coerced by this route's response_model
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
verbose_proxy_logger.exception("Error in model %s: %s", action, e)
|
verbose_proxy_logger.exception("Error in model %s: %s", action, e)
|
||||||
|
|
@ -897,7 +909,7 @@ async def _add_model_to_db(
|
||||||
prisma_client: PrismaClient,
|
prisma_client: PrismaClient,
|
||||||
new_encryption_key: str | None = None,
|
new_encryption_key: str | None = None,
|
||||||
should_create_model_in_db: bool = True,
|
should_create_model_in_db: bool = True,
|
||||||
) -> LiteLLM_ProxyModelTable | None:
|
) -> "prisma_models.LiteLLM_ProxyModelTable | LiteLLM_ProxyModelTable | None":
|
||||||
# encrypt litellm params #
|
# encrypt litellm params #
|
||||||
_litellm_params_dict: Final = model_params.litellm_params.dict(exclude_none=True)
|
_litellm_params_dict: Final = model_params.litellm_params.dict(exclude_none=True)
|
||||||
_original_litellm_model_name: Final = model_params.litellm_params.model
|
_original_litellm_model_name: Final = model_params.litellm_params.model
|
||||||
|
|
@ -914,8 +926,9 @@ async def _add_model_to_db(
|
||||||
}
|
}
|
||||||
if model_params.model_info.id is not None:
|
if model_params.model_info.id is not None:
|
||||||
_data["model_id"] = model_params.model_info.id
|
_data["model_id"] = model_params.model_info.id
|
||||||
|
_create_data: Final = cast("Mapping[str, object]", _data) # cast-ok: str-keyed json payload built just above
|
||||||
if should_create_model_in_db:
|
if should_create_model_in_db:
|
||||||
model_response = await ModelRepository(prisma_client).table.create(data=_data)
|
model_response = await ModelRepository(prisma_client).table.create(data=_create_data)
|
||||||
else:
|
else:
|
||||||
model_response = LiteLLM_ProxyModelTable(**_data)
|
model_response = LiteLLM_ProxyModelTable(**_data)
|
||||||
return model_response
|
return model_response
|
||||||
|
|
@ -925,7 +938,7 @@ async def _add_team_model_to_db(
|
||||||
model_params: Deployment,
|
model_params: Deployment,
|
||||||
user_api_key_dict: UserAPIKeyAuth,
|
user_api_key_dict: UserAPIKeyAuth,
|
||||||
prisma_client: PrismaClient,
|
prisma_client: PrismaClient,
|
||||||
) -> LiteLLM_ProxyModelTable | None:
|
) -> "prisma_models.LiteLLM_ProxyModelTable | LiteLLM_ProxyModelTable | None":
|
||||||
"""
|
"""
|
||||||
If 'team_id' is provided,
|
If 'team_id' is provided,
|
||||||
|
|
||||||
|
|
@ -1638,7 +1651,9 @@ async def delete_team_model_alias(
|
||||||
tasks: Final = []
|
tasks: Final = []
|
||||||
removed_model_aliases: Final[list[tuple[str, str]]] = []
|
removed_model_aliases: Final[list[tuple[str, str]]] = []
|
||||||
for team_model_alias in team_model_aliases:
|
for team_model_alias in team_model_aliases:
|
||||||
model_aliases = team_model_alias.model_aliases # {"alias": "public model name"}
|
model_aliases = cast( # cast-ok: prisma types Json columns as `str`; the driver hands back the parsed dict
|
||||||
|
"dict[str, str]", team_model_alias.model_aliases
|
||||||
|
)
|
||||||
id = team_model_alias.id
|
id = team_model_alias.id
|
||||||
|
|
||||||
if public_model_name in model_aliases.values():
|
if public_model_name in model_aliases.values():
|
||||||
|
|
@ -1733,7 +1748,7 @@ async def add_new_model(
|
||||||
existing_params=None,
|
existing_params=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
model_response: LiteLLM_ProxyModelTable | None = None
|
model_response: prisma_models.LiteLLM_ProxyModelTable | LiteLLM_ProxyModelTable | None = None
|
||||||
# update DB
|
# update DB
|
||||||
incoming_model_info: Final = model_params.model_info.model_dump(exclude_none=True)
|
incoming_model_info: Final = model_params.model_info.model_dump(exclude_none=True)
|
||||||
_raise_if_ptu_cost_attribution_disabled(incoming_model_info)
|
_raise_if_ptu_cost_attribution_disabled(incoming_model_info)
|
||||||
|
|
@ -1902,7 +1917,10 @@ async def update_model(
|
||||||
|
|
||||||
# update DB
|
# update DB
|
||||||
if store_model_in_db is True:
|
if store_model_in_db is True:
|
||||||
_existing_litellm_params_dict: Final = dict(_existing_litellm_params.litellm_params)
|
existing_model_row: Final = cast( # cast-ok: prisma types Json columns as `str`; the driver parses them
|
||||||
|
"_ExistingModelRow", _existing_litellm_params
|
||||||
|
)
|
||||||
|
_existing_litellm_params_dict: Final = dict(existing_model_row.litellm_params)
|
||||||
|
|
||||||
if model_params.litellm_params is None:
|
if model_params.litellm_params is None:
|
||||||
raise Exception("litellm_params not provided")
|
raise Exception("litellm_params not provided")
|
||||||
|
|
@ -1946,8 +1964,8 @@ async def update_model(
|
||||||
user_api_key_dict=user_api_key_dict,
|
user_api_key_dict=user_api_key_dict,
|
||||||
table_name=LitellmTableNames.PROXY_MODEL_TABLE_NAME,
|
table_name=LitellmTableNames.PROXY_MODEL_TABLE_NAME,
|
||||||
before_value=(
|
before_value=(
|
||||||
_existing_litellm_params.model_dump_json(exclude_none=True)
|
existing_model_row.model_dump_json(exclude_none=True)
|
||||||
if isinstance(_existing_litellm_params, BaseModel)
|
if isinstance(existing_model_row, BaseModel)
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
after_value=(
|
after_value=(
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,14 @@ Endpoints for /organization operations
|
||||||
#### ORGANIZATION MANAGEMENT ####
|
#### ORGANIZATION MANAGEMENT ####
|
||||||
|
|
||||||
from collections.abc import Mapping, Sequence
|
from collections.abc import Mapping, Sequence
|
||||||
from typing import TYPE_CHECKING, Annotated, Final, Protocol, overload
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Annotated,
|
||||||
|
Final,
|
||||||
|
Protocol,
|
||||||
|
cast, # noqa: TID251 # prisma types Json columns as fields.Json but reads back plain python values
|
||||||
|
overload,
|
||||||
|
)
|
||||||
|
|
||||||
import fastapi
|
import fastapi
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||||
|
|
@ -74,6 +81,11 @@ if TYPE_CHECKING:
|
||||||
router: Final = APIRouter()
|
router: Final = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
class _ObjectPermissionRow(Protocol):
|
||||||
|
@property
|
||||||
|
def object_permission_id(self) -> str | None: ...
|
||||||
|
|
||||||
|
|
||||||
class _UserTableClient(Protocol):
|
class _UserTableClient(Protocol):
|
||||||
async def find_unique(self, where: Mapping[str, object]) -> "PrismaUserTable | None": ...
|
async def find_unique(self, where: Mapping[str, object]) -> "PrismaUserTable | None": ...
|
||||||
|
|
||||||
|
|
@ -681,7 +693,10 @@ async def update_organization(
|
||||||
existing_metadata: Final = existing_organization_row.metadata or {}
|
existing_metadata: Final = existing_organization_row.metadata or {}
|
||||||
updated_metadata: Final = updated_organization_row_json.get("metadata", {})
|
updated_metadata: Final = updated_organization_row_json.get("metadata", {})
|
||||||
merged_metadata: Final[Mapping[str, object]] = _update_dictionary(
|
merged_metadata: Final[Mapping[str, object]] = _update_dictionary(
|
||||||
existing_dict=existing_metadata.copy(), new_dict=updated_metadata
|
existing_dict=cast( # cast-ok: prisma de-serializes a Json column to the plain python dict it stores
|
||||||
|
"dict[str, object]", existing_metadata
|
||||||
|
).copy(),
|
||||||
|
new_dict=updated_metadata,
|
||||||
)
|
)
|
||||||
updated_organization_row_json["metadata"] = merged_metadata
|
updated_organization_row_json["metadata"] = merged_metadata
|
||||||
|
|
||||||
|
|
@ -720,7 +735,7 @@ async def update_organization(
|
||||||
|
|
||||||
async def handle_update_object_permission(
|
async def handle_update_object_permission(
|
||||||
data_json: dict[str, object],
|
data_json: dict[str, object],
|
||||||
existing_organization_row: LiteLLM_OrganizationTable,
|
existing_organization_row: _ObjectPermissionRow,
|
||||||
) -> dict[str, object]:
|
) -> dict[str, object]:
|
||||||
"""
|
"""
|
||||||
Handle the update of object permission for an organization.
|
Handle the update of object permission for an organization.
|
||||||
|
|
@ -1276,17 +1291,20 @@ async def find_member_if_email(user_email: str, prisma_client: PrismaClient) ->
|
||||||
Find a member if the user_email is in LiteLLM_UserTable
|
Find a member if the user_email is in LiteLLM_UserTable
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
not_unique_user_email_error: Final = HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail={
|
||||||
|
"error": f"Unique user not found for user_email={user_email}. Potential duplicate OR non-existent user_email in LiteLLM_UserTable. Use 'user_id' instead."
|
||||||
|
},
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
existing_user_email_row: Final[BaseModel] = await UserRepository(prisma_client).table.find_unique(
|
existing_user_email_row: Final = await UserRepository(prisma_client).table.find_unique(
|
||||||
where={"user_email": user_email}
|
where={"user_email": user_email}
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
raise HTTPException(
|
raise not_unique_user_email_error
|
||||||
status_code=400,
|
if existing_user_email_row is None:
|
||||||
detail={
|
raise not_unique_user_email_error
|
||||||
"error": f"Unique user not found for user_email={user_email}. Potential duplicate OR non-existent user_email in LiteLLM_UserTable. Use 'user_id' instead."
|
|
||||||
},
|
|
||||||
)
|
|
||||||
existing_user_email_row_pydantic: Final = LiteLLM_UserTable.model_validate(existing_user_email_row.model_dump())
|
existing_user_email_row_pydantic: Final = LiteLLM_UserTable.model_validate(existing_user_email_row.model_dump())
|
||||||
return existing_user_email_row_pydantic
|
return existing_user_email_row_pydantic
|
||||||
|
|
||||||
|
|
@ -1537,7 +1555,10 @@ async def add_member_to_organization(
|
||||||
_returned_user = await prisma_client.insert_data(data=new_user_defaults, table_name="user")
|
_returned_user = await prisma_client.insert_data(data=new_user_defaults, table_name="user")
|
||||||
if _returned_user is not None:
|
if _returned_user is not None:
|
||||||
user_object = LiteLLM_UserTable.model_validate(_returned_user.model_dump())
|
user_object = LiteLLM_UserTable.model_validate(_returned_user.model_dump())
|
||||||
elif existing_user_email_row is not None and len(existing_user_email_row) > 1:
|
elif existing_user_email_row is not None and (
|
||||||
|
len(existing_user_email_row) # pyright: ignore[reportArgumentType] # find_unique yields a row, not a list
|
||||||
|
> 1
|
||||||
|
):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail={"error": "Multiple users with this email found in db. Please use 'user_id' instead."},
|
detail={"error": "Multiple users with this email found in db. Please use 'user_id' instead."},
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,8 @@ class ScimTransformations:
|
||||||
|
|
||||||
# Get user's teams/groups
|
# Get user's teams/groups
|
||||||
groups: Final = []
|
groups: Final = []
|
||||||
for team_id in user.teams or []:
|
team_ids: Final[list[str]] = user.teams or [] # mutable-ok: scim reads the user row's team ids
|
||||||
|
for team_id in team_ids:
|
||||||
team = await TeamRepository(prisma_client).table.find_unique(where={"team_id": team_id})
|
team = await TeamRepository(prisma_client).table.find_unique(where={"team_id": team_id})
|
||||||
if team:
|
if team:
|
||||||
team_alias = getattr(team, "team_alias", team.team_id)
|
team_alias = getattr(team, "team_alias", team.team_id)
|
||||||
|
|
|
||||||
|
|
@ -2766,6 +2766,12 @@ async def patch_group(
|
||||||
if final_team:
|
if final_team:
|
||||||
updated_team = final_team
|
updated_team = final_team
|
||||||
|
|
||||||
|
if updated_team is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail={"error": f"Group not found with ID: {group_id}"}, # mutable-ok: FastAPI detail contract
|
||||||
|
)
|
||||||
|
|
||||||
# Convert to SCIM format and return
|
# Convert to SCIM format and return
|
||||||
scim_group: Final = await ScimTransformations.transform_litellm_team_to_scim_group(
|
scim_group: Final = await ScimTransformations.transform_litellm_team_to_scim_group(
|
||||||
LiteLLM_TeamTable.model_validate(updated_team.model_dump())
|
LiteLLM_TeamTable.model_validate(updated_team.model_dump())
|
||||||
|
|
|
||||||
|
|
@ -369,10 +369,10 @@ async def _add_tag_to_deployment(deployment: "Deployment", tag: str):
|
||||||
|
|
||||||
# Prisma returns litellm_params as dict (already parsed from JSON)
|
# Prisma returns litellm_params as dict (already parsed from JSON)
|
||||||
existing_params = db_model.litellm_params
|
existing_params = db_model.litellm_params
|
||||||
if isinstance(existing_params, str):
|
if isinstance(existing_params, str): # pyright: ignore[reportUnnecessaryIsInstance] # prisma Json stub is str
|
||||||
# If it's a string, parse it
|
# If it's a string, parse it
|
||||||
existing_params = json.loads(existing_params)
|
existing_params = json.loads(existing_params)
|
||||||
elif not isinstance(existing_params, dict):
|
elif not isinstance(existing_params, dict): # pyright: ignore[reportUnnecessaryIsInstance] # prisma Json stub
|
||||||
raise Exception(f"Unexpected litellm_params type: {type(existing_params)}")
|
raise Exception(f"Unexpected litellm_params type: {type(existing_params)}")
|
||||||
|
|
||||||
# Add tag to tags array (preserve encryption of other fields)
|
# Add tag to tags array (preserve encryption of other fields)
|
||||||
|
|
|
||||||
|
|
@ -352,6 +352,9 @@ async def add_team_callbacks(
|
||||||
include={"object_permission": True}, # mutable-ok: prisma include takes a dict literal
|
include={"object_permission": True}, # mutable-ok: prisma include takes a dict literal
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if new_team_row is None:
|
||||||
|
raise _callback_error(400, f"Team id = {team_id} does not exist. Please use a different team id.")
|
||||||
|
|
||||||
# Without this a newly registered callback stays dormant for existing keys.
|
# Without this a newly registered callback stays dormant for existing keys.
|
||||||
await _refresh_cached_team(
|
await _refresh_cached_team(
|
||||||
team_row=new_team_row,
|
team_row=new_team_row,
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import traceback
|
||||||
from collections.abc import Mapping, Sequence
|
from collections.abc import Mapping, Sequence
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from types import MappingProxyType
|
from types import MappingProxyType
|
||||||
from typing import Annotated, Final, NamedTuple, NoReturn, Protocol, TypeVar, cast
|
from typing import TYPE_CHECKING, Annotated, Final, NamedTuple, NoReturn, Protocol, TypeVar, cast
|
||||||
|
|
||||||
import fastapi
|
import fastapi
|
||||||
from fastapi import APIRouter, Depends, Header, HTTPException, Request, status
|
from fastapi import APIRouter, Depends, Header, HTTPException, Request, status
|
||||||
|
|
@ -34,21 +34,17 @@ from litellm.proxy._types import (
|
||||||
BudgetNewRequest,
|
BudgetNewRequest,
|
||||||
CommonProxyErrors,
|
CommonProxyErrors,
|
||||||
DeleteTeamRequest,
|
DeleteTeamRequest,
|
||||||
LiteLLM_AccessGroupTable,
|
|
||||||
LiteLLM_AuditLogs,
|
LiteLLM_AuditLogs,
|
||||||
LiteLLM_BudgetTableFull,
|
|
||||||
LiteLLM_DeletedTeamTable,
|
LiteLLM_DeletedTeamTable,
|
||||||
LiteLLM_ManagementEndpoint_MetadataFields,
|
LiteLLM_ManagementEndpoint_MetadataFields,
|
||||||
LiteLLM_ManagementEndpoint_MetadataFields_Premium,
|
LiteLLM_ManagementEndpoint_MetadataFields_Premium,
|
||||||
LiteLLM_ModelTable,
|
LiteLLM_ModelTable,
|
||||||
LiteLLM_OrganizationMembershipTable,
|
|
||||||
LiteLLM_OrganizationTable,
|
LiteLLM_OrganizationTable,
|
||||||
LiteLLM_OrganizationTableWithMembers,
|
LiteLLM_OrganizationTableWithMembers,
|
||||||
LiteLLM_TeamMembership,
|
LiteLLM_TeamMembership,
|
||||||
LiteLLM_TeamTable,
|
LiteLLM_TeamTable,
|
||||||
LiteLLM_TeamTableCachedObj,
|
LiteLLM_TeamTableCachedObj,
|
||||||
LiteLLM_UserTable,
|
LiteLLM_UserTable,
|
||||||
LiteLLM_VerificationToken,
|
|
||||||
LitellmTableNames,
|
LitellmTableNames,
|
||||||
LitellmUserRoles,
|
LitellmUserRoles,
|
||||||
Member,
|
Member,
|
||||||
|
|
@ -143,6 +139,7 @@ from litellm.proxy.management_helpers.utils import (
|
||||||
from litellm.proxy.utils import PrismaClient, ProxyLogging, 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.budget_repository import BudgetRepository
|
||||||
from litellm.repositories.organization_repository import OrganizationRepository
|
from litellm.repositories.organization_repository import OrganizationRepository
|
||||||
|
from litellm.repositories.prisma_protocols import TableActions
|
||||||
from litellm.repositories.table_repositories import (
|
from litellm.repositories.table_repositories import (
|
||||||
AccessGroupRepository,
|
AccessGroupRepository,
|
||||||
DeletedTeamRepository,
|
DeletedTeamRepository,
|
||||||
|
|
@ -174,6 +171,10 @@ from litellm.types.proxy.management_endpoints.team_endpoints import (
|
||||||
UpdateTeamMemberPermissionsRequest,
|
UpdateTeamMemberPermissionsRequest,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from prisma import Prisma
|
||||||
|
from prisma import models as prisma_models
|
||||||
|
|
||||||
router: Final = APIRouter()
|
router: Final = APIRouter()
|
||||||
|
|
||||||
_DbRecordT = TypeVar("_DbRecordT")
|
_DbRecordT = TypeVar("_DbRecordT")
|
||||||
|
|
@ -188,95 +189,14 @@ class _TeamIdGroupRow(TypedDict):
|
||||||
_count: _TeamIdKeyCount
|
_count: _TeamIdKeyCount
|
||||||
|
|
||||||
|
|
||||||
class _PrismaTableActions(Protocol[_DbRecordT]):
|
|
||||||
async def find_unique(
|
|
||||||
self,
|
|
||||||
where: Mapping[str, object],
|
|
||||||
include: Mapping[str, bool] | None = None,
|
|
||||||
) -> _DbRecordT | None: ...
|
|
||||||
|
|
||||||
async def find_first(
|
|
||||||
self,
|
|
||||||
where: Mapping[str, object] | None = None,
|
|
||||||
order: Mapping[str, str] | None = None,
|
|
||||||
) -> _DbRecordT | None: ...
|
|
||||||
|
|
||||||
async def find_many(
|
|
||||||
self,
|
|
||||||
where: Mapping[str, object] | None = None,
|
|
||||||
include: Mapping[str, bool] | None = None,
|
|
||||||
order: Mapping[str, str] | None = None,
|
|
||||||
skip: int | None = None,
|
|
||||||
take: int | None = None,
|
|
||||||
cursor: Mapping[str, object] | None = None,
|
|
||||||
) -> list[_DbRecordT]: ...
|
|
||||||
|
|
||||||
async def create(
|
|
||||||
self,
|
|
||||||
data: Mapping[str, object],
|
|
||||||
include: Mapping[str, bool] | None = None,
|
|
||||||
) -> _DbRecordT: ...
|
|
||||||
|
|
||||||
async def create_many(
|
|
||||||
self,
|
|
||||||
data: Sequence[Mapping[str, object]],
|
|
||||||
skip_duplicates: bool | None = None,
|
|
||||||
) -> int: ...
|
|
||||||
|
|
||||||
async def update(
|
|
||||||
self,
|
|
||||||
where: Mapping[str, object],
|
|
||||||
data: Mapping[str, object],
|
|
||||||
include: Mapping[str, bool] | None = None,
|
|
||||||
) -> _DbRecordT: ...
|
|
||||||
|
|
||||||
async def update_many(
|
|
||||||
self,
|
|
||||||
where: Mapping[str, object],
|
|
||||||
data: Mapping[str, object],
|
|
||||||
) -> int: ...
|
|
||||||
|
|
||||||
async def upsert(
|
|
||||||
self,
|
|
||||||
where: Mapping[str, object],
|
|
||||||
data: Mapping[str, Mapping[str, object]],
|
|
||||||
) -> _DbRecordT: ...
|
|
||||||
|
|
||||||
async def delete_many(
|
|
||||||
self,
|
|
||||||
where: Mapping[str, object] | None = None,
|
|
||||||
) -> int: ...
|
|
||||||
|
|
||||||
async def count(
|
|
||||||
self,
|
|
||||||
where: Mapping[str, object] | None = None,
|
|
||||||
) -> int: ...
|
|
||||||
|
|
||||||
async def group_by(
|
|
||||||
self,
|
|
||||||
by: Sequence[str],
|
|
||||||
where: Mapping[str, object] | None = None,
|
|
||||||
count: Mapping[str, bool] | None = None,
|
|
||||||
) -> Sequence[_TeamIdGroupRow]: ...
|
|
||||||
|
|
||||||
|
|
||||||
class _HasTableActions(Protocol[_DbRecordT]):
|
|
||||||
@property
|
|
||||||
def table(self) -> "_PrismaTableActions[_DbRecordT]": ...
|
|
||||||
|
|
||||||
|
|
||||||
def _typed_table(
|
|
||||||
repo: "_HasTableActions[_DbRecordT]", record_type: type[_DbRecordT]
|
|
||||||
) -> "_PrismaTableActions[_DbRecordT]":
|
|
||||||
return repo.table
|
|
||||||
|
|
||||||
|
|
||||||
def _as_object(value: object) -> object:
|
def _as_object(value: object) -> object:
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
def _nullable(value: _DbRecordT | None) -> _DbRecordT | None:
|
def _as_list(rows: Sequence[_DbRecordT]) -> list[_DbRecordT]: # mutable-ok: pydantic list[...] fields reject Sequence
|
||||||
return value
|
return cast( # cast-ok: prisma-client-py find_many returns a list; TableActions only widens it to Sequence
|
||||||
|
"list[_DbRecordT]", rows
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class _UserIdRow(Protocol):
|
class _UserIdRow(Protocol):
|
||||||
|
|
@ -284,33 +204,75 @@ class _UserIdRow(Protocol):
|
||||||
def user_id(self) -> str | None: ...
|
def user_id(self) -> str | None: ...
|
||||||
|
|
||||||
|
|
||||||
class _HasUserIdTable(Protocol):
|
def _user_id_rows_db(repo: UserRepository) -> "TableActions[_UserIdRow]":
|
||||||
@property
|
|
||||||
def table(self) -> "_PrismaTableActions[_UserIdRow]": ...
|
|
||||||
|
|
||||||
|
|
||||||
def _user_id_rows_db(repo: "_HasUserIdTable") -> "_PrismaTableActions[_UserIdRow]":
|
|
||||||
return repo.table
|
return repo.table
|
||||||
|
|
||||||
|
|
||||||
class _RawTeamRow(Protocol):
|
class _ModelDumpRow(Protocol):
|
||||||
|
def model_dump(self) -> Mapping[str, object]: ...
|
||||||
|
|
||||||
|
|
||||||
|
class _TeamIdRow(Protocol):
|
||||||
@property
|
@property
|
||||||
def members_with_roles(self) -> Sequence[Mapping[str, object]] | None: ...
|
def team_id(self) -> str: ...
|
||||||
|
|
||||||
|
|
||||||
class _HasRawTeamTable(Protocol):
|
class _CacheableTeamRow(_TeamIdRow, _ModelDumpRow, Protocol): ...
|
||||||
|
|
||||||
|
|
||||||
|
class _ObjectPermissionRow(Protocol):
|
||||||
@property
|
@property
|
||||||
def table(self) -> "_PrismaTableActions[_RawTeamRow]": ...
|
def object_permission_id(self) -> str | None: ...
|
||||||
|
|
||||||
|
|
||||||
def _raw_team_db(repo: "_HasRawTeamTable") -> "_PrismaTableActions[_RawTeamRow]":
|
class _TeamAliasBudgetRow(Protocol):
|
||||||
return repo.table
|
@property
|
||||||
|
def team_alias(self) -> str | None: ...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def budget_duration(self) -> str | None: ...
|
||||||
|
|
||||||
|
|
||||||
|
class _TeamBudgetRow(_TeamAliasBudgetRow, Protocol):
|
||||||
|
metadata: Mapping[str, JsonValue] | None
|
||||||
|
|
||||||
|
|
||||||
|
class _AuditableTeamRow(Protocol):
|
||||||
|
def json(self, *, exclude_none: bool = False) -> str: ...
|
||||||
|
|
||||||
|
|
||||||
|
class _RawTeamRow(_TeamIdRow, _ModelDumpRow, _ObjectPermissionRow, _TeamBudgetRow, _AuditableTeamRow, Protocol):
|
||||||
|
@property
|
||||||
|
def members_with_roles(
|
||||||
|
self,
|
||||||
|
) -> Sequence[dict[str, object]] | None: ... # mutable-ok: prisma deserializes this JSON column into plain dicts
|
||||||
|
|
||||||
|
@property
|
||||||
|
def organization_id(self) -> str | None: ...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_budget(self) -> float | None: ...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def soft_budget(self) -> float | None: ...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def model_id(self) -> int | None: ...
|
||||||
|
|
||||||
|
|
||||||
|
def _raw_team_db(repo: TeamRepository) -> "TableActions[_RawTeamRow]":
|
||||||
|
return cast( # cast-ok: prisma types Json columns as str; the client hands back the deserialized value
|
||||||
|
"TableActions[_RawTeamRow]", repo.table
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _BudgetIdRow(Protocol):
|
||||||
|
@property
|
||||||
|
def budget_id(self) -> str: ...
|
||||||
|
|
||||||
|
|
||||||
class _BudgetWriteCall(Protocol):
|
class _BudgetWriteCall(Protocol):
|
||||||
async def __call__(
|
async def __call__(self, budget_obj: BudgetNewRequest, user_api_key_dict: UserAPIKeyAuth) -> _BudgetIdRow: ...
|
||||||
self, budget_obj: BudgetNewRequest, user_api_key_dict: UserAPIKeyAuth
|
|
||||||
) -> LiteLLM_BudgetTableFull: ...
|
|
||||||
|
|
||||||
|
|
||||||
def _as_budget_write(fn: "_BudgetWriteCall") -> "_BudgetWriteCall":
|
def _as_budget_write(fn: "_BudgetWriteCall") -> "_BudgetWriteCall":
|
||||||
|
|
@ -343,7 +305,7 @@ class _ErrorDetail(TypedDict):
|
||||||
|
|
||||||
class _TeamCreateTx(AccessGroupSyncTx, Protocol):
|
class _TeamCreateTx(AccessGroupSyncTx, Protocol):
|
||||||
@property
|
@property
|
||||||
def litellm_teamtable(self) -> "_PrismaTableActions[LiteLLM_TeamTable]": ...
|
def litellm_teamtable(self) -> "TableActions[prisma_models.LiteLLM_TeamTable]": ...
|
||||||
|
|
||||||
|
|
||||||
class _MemberDeleteTx(Protocol):
|
class _MemberDeleteTx(Protocol):
|
||||||
|
|
@ -355,20 +317,20 @@ class _MemberDeleteTx(Protocol):
|
||||||
of them hold the rest of the pool."""
|
of them hold the rest of the pool."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def litellm_usertable(self) -> "_PrismaTableActions[LiteLLM_UserTable]": ...
|
def litellm_usertable(self) -> "TableActions[prisma_models.LiteLLM_UserTable]": ...
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def litellm_verificationtoken(self) -> "_PrismaTableActions[LiteLLM_VerificationToken]": ...
|
def litellm_verificationtoken(self) -> "TableActions[prisma_models.LiteLLM_VerificationToken]": ...
|
||||||
|
|
||||||
|
|
||||||
class _TeamDeleteTx(AccessGroupSyncTx, Protocol):
|
class _TeamDeleteTx(AccessGroupSyncTx, Protocol):
|
||||||
async def execute_raw(self, query: str, *args: object) -> int: ...
|
async def execute_raw(self, query: str, *args: object) -> int: ...
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def litellm_teamtable(self) -> "_PrismaTableActions[LiteLLM_TeamTable]": ...
|
def litellm_teamtable(self) -> "TableActions[prisma_models.LiteLLM_TeamTable]": ...
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def litellm_teammembership(self) -> "_PrismaTableActions[LiteLLM_TeamMembership]": ...
|
def litellm_teammembership(self) -> "TableActions[prisma_models.LiteLLM_TeamMembership]": ...
|
||||||
|
|
||||||
|
|
||||||
_STRIP_DELETED_TEAM_FROM_USERS_SQL: Final = """
|
_STRIP_DELETED_TEAM_FROM_USERS_SQL: Final = """
|
||||||
|
|
@ -378,46 +340,52 @@ UPDATE "LiteLLM_UserTable" SET teams = array_remove(teams, $1) WHERE $1 = ANY(te
|
||||||
_INCLUDE_MODEL_TABLE: Final = MappingProxyType({"litellm_model_table": True})
|
_INCLUDE_MODEL_TABLE: Final = MappingProxyType({"litellm_model_table": True})
|
||||||
|
|
||||||
|
|
||||||
def _team_db(prisma_client: PrismaClient | None) -> "_PrismaTableActions[LiteLLM_TeamTable]":
|
def _team_db(prisma_client: PrismaClient | None) -> "TableActions[prisma_models.LiteLLM_TeamTable]":
|
||||||
return _typed_table(TeamRepository(prisma_client), LiteLLM_TeamTable)
|
return TeamRepository(prisma_client).table
|
||||||
|
|
||||||
|
|
||||||
def _team_membership_db(prisma_client: PrismaClient | None) -> "_PrismaTableActions[LiteLLM_TeamMembership]":
|
def _team_tx_db(tx: "Prisma") -> "TableActions[prisma_models.LiteLLM_TeamTable]":
|
||||||
return _typed_table(TeamMembershipRepository(prisma_client), LiteLLM_TeamMembership)
|
return cast( # cast-ok: generated actions type Json columns as str; TableActions widens inputs to Mapping
|
||||||
|
"TableActions[prisma_models.LiteLLM_TeamTable]", tx.litellm_teamtable
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _user_db(prisma_client: PrismaClient | None) -> "_PrismaTableActions[LiteLLM_UserTable]":
|
def _team_membership_db(prisma_client: PrismaClient | None) -> "TableActions[prisma_models.LiteLLM_TeamMembership]":
|
||||||
return _typed_table(UserRepository(prisma_client), LiteLLM_UserTable)
|
return TeamMembershipRepository(prisma_client).table
|
||||||
|
|
||||||
|
|
||||||
def _model_db(prisma_client: PrismaClient | None) -> "_PrismaTableActions[LiteLLM_ModelTable]":
|
def _user_db(prisma_client: PrismaClient | None) -> "TableActions[prisma_models.LiteLLM_UserTable]":
|
||||||
return _typed_table(ModelTableRepository(prisma_client), LiteLLM_ModelTable)
|
return UserRepository(prisma_client).table
|
||||||
|
|
||||||
|
|
||||||
def _org_db(prisma_client: PrismaClient | None) -> "_PrismaTableActions[LiteLLM_OrganizationTable]":
|
def _model_db(prisma_client: PrismaClient | None) -> "TableActions[prisma_models.LiteLLM_ModelTable]":
|
||||||
return _typed_table(OrganizationRepository(prisma_client), LiteLLM_OrganizationTable)
|
return ModelTableRepository(prisma_client).table
|
||||||
|
|
||||||
|
|
||||||
|
def _org_db(prisma_client: PrismaClient | None) -> "TableActions[prisma_models.LiteLLM_OrganizationTable]":
|
||||||
|
return OrganizationRepository(prisma_client).table
|
||||||
|
|
||||||
|
|
||||||
def _org_membership_db(
|
def _org_membership_db(
|
||||||
prisma_client: PrismaClient | None,
|
prisma_client: PrismaClient | None,
|
||||||
) -> "_PrismaTableActions[LiteLLM_OrganizationMembershipTable]":
|
) -> "TableActions[prisma_models.LiteLLM_OrganizationMembership]":
|
||||||
return _typed_table(OrganizationMembershipRepository(prisma_client), LiteLLM_OrganizationMembershipTable)
|
return OrganizationMembershipRepository(prisma_client).table
|
||||||
|
|
||||||
|
|
||||||
def _budget_db(prisma_client: PrismaClient | None) -> "_PrismaTableActions[LiteLLM_BudgetTableFull]":
|
def _budget_db(prisma_client: PrismaClient | None) -> "TableActions[prisma_models.LiteLLM_BudgetTable]":
|
||||||
return _typed_table(BudgetRepository(prisma_client), LiteLLM_BudgetTableFull)
|
return BudgetRepository(prisma_client).table
|
||||||
|
|
||||||
|
|
||||||
def _deleted_team_db(prisma_client: PrismaClient | None) -> "_PrismaTableActions[LiteLLM_DeletedTeamTable]":
|
def _deleted_team_db(prisma_client: PrismaClient | None) -> "TableActions[prisma_models.LiteLLM_DeletedTeamTable]":
|
||||||
return _typed_table(DeletedTeamRepository(prisma_client), LiteLLM_DeletedTeamTable)
|
return DeletedTeamRepository(prisma_client).table
|
||||||
|
|
||||||
|
|
||||||
def _access_group_db(prisma_client: PrismaClient | None) -> "_PrismaTableActions[LiteLLM_AccessGroupTable]":
|
def _access_group_db(prisma_client: PrismaClient | None) -> "TableActions[prisma_models.LiteLLM_AccessGroupTable]":
|
||||||
return _typed_table(AccessGroupRepository(prisma_client), LiteLLM_AccessGroupTable)
|
return AccessGroupRepository(prisma_client).table
|
||||||
|
|
||||||
|
|
||||||
def _tokens_db(prisma_client: PrismaClient | None) -> "_PrismaTableActions[LiteLLM_VerificationToken]":
|
def _tokens_db(prisma_client: PrismaClient | None) -> "TableActions[prisma_models.LiteLLM_VerificationToken]":
|
||||||
return _typed_table(VerificationTokenRepository(prisma_client), LiteLLM_VerificationToken)
|
return VerificationTokenRepository(prisma_client).table
|
||||||
|
|
||||||
|
|
||||||
def _sanitize_for_log(value: object) -> str:
|
def _sanitize_for_log(value: object) -> str:
|
||||||
|
|
@ -430,7 +398,7 @@ def _sanitize_for_log(value: object) -> str:
|
||||||
|
|
||||||
|
|
||||||
async def _refresh_cached_team(
|
async def _refresh_cached_team(
|
||||||
team_row: LiteLLM_TeamTable,
|
team_row: _CacheableTeamRow,
|
||||||
user_api_key_cache: UserApiKeyCache,
|
user_api_key_cache: UserApiKeyCache,
|
||||||
proxy_logging_obj: ProxyLogging,
|
proxy_logging_obj: ProxyLogging,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
@ -519,7 +487,7 @@ class TeamMemberBudgetHandler:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def create_team_member_budget_table(
|
async def create_team_member_budget_table(
|
||||||
data: NewTeamRequest | LiteLLM_TeamTable,
|
data: NewTeamRequest | _TeamAliasBudgetRow,
|
||||||
new_team_data_json: dict,
|
new_team_data_json: dict,
|
||||||
user_api_key_dict: UserAPIKeyAuth,
|
user_api_key_dict: UserAPIKeyAuth,
|
||||||
team_member_budget: float | None = None,
|
team_member_budget: float | None = None,
|
||||||
|
|
@ -570,7 +538,7 @@ class TeamMemberBudgetHandler:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def upsert_team_member_budget_table(
|
async def upsert_team_member_budget_table(
|
||||||
team_table: LiteLLM_TeamTable,
|
team_table: _TeamBudgetRow,
|
||||||
user_api_key_dict: UserAPIKeyAuth,
|
user_api_key_dict: UserAPIKeyAuth,
|
||||||
updated_kv: dict,
|
updated_kv: dict,
|
||||||
team_member_budget: float | None = None,
|
team_member_budget: float | None = None,
|
||||||
|
|
@ -641,7 +609,7 @@ class TeamMemberBudgetHandler:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def clear_team_member_budget_fields(
|
async def clear_team_member_budget_fields(
|
||||||
team_table: LiteLLM_TeamTable,
|
team_table: _TeamBudgetRow,
|
||||||
user_api_key_dict: "UserAPIKeyAuth",
|
user_api_key_dict: "UserAPIKeyAuth",
|
||||||
updated_kv: dict,
|
updated_kv: dict,
|
||||||
explicitly_set_fields: set,
|
explicitly_set_fields: set,
|
||||||
|
|
@ -1578,7 +1546,7 @@ async def new_team(
|
||||||
|
|
||||||
tx: _TeamCreateTx
|
tx: _TeamCreateTx
|
||||||
async with prisma_client.db.tx() as tx:
|
async with prisma_client.db.tx() as tx:
|
||||||
team_row: Final[LiteLLM_TeamTable] = await tx.litellm_teamtable.create(
|
team_row: Final[prisma_models.LiteLLM_TeamTable] = await tx.litellm_teamtable.create(
|
||||||
data=team_creation_data,
|
data=team_creation_data,
|
||||||
include=_INCLUDE_MODEL_TABLE,
|
include=_INCLUDE_MODEL_TABLE,
|
||||||
)
|
)
|
||||||
|
|
@ -1633,7 +1601,7 @@ async def new_team(
|
||||||
|
|
||||||
|
|
||||||
async def _create_team_update_audit_log(
|
async def _create_team_update_audit_log(
|
||||||
existing_team_row: LiteLLM_TeamTable,
|
existing_team_row: _AuditableTeamRow,
|
||||||
updated_kv: dict,
|
updated_kv: dict,
|
||||||
team_id: str,
|
team_id: str,
|
||||||
litellm_changed_by: str | None,
|
litellm_changed_by: str | None,
|
||||||
|
|
@ -1756,11 +1724,11 @@ async def _auto_add_team_members_to_organization(
|
||||||
|
|
||||||
async def fetch_and_validate_organization(
|
async def fetch_and_validate_organization(
|
||||||
organization_id: str,
|
organization_id: str,
|
||||||
existing_team_row: LiteLLM_TeamTable,
|
existing_team_row: _ModelDumpRow,
|
||||||
llm_router: Router | None,
|
llm_router: Router | None,
|
||||||
prisma_client: PrismaClient,
|
prisma_client: PrismaClient,
|
||||||
user_api_key_dict: UserAPIKeyAuth | None = None,
|
user_api_key_dict: UserAPIKeyAuth | None = None,
|
||||||
) -> LiteLLM_OrganizationTable:
|
) -> "prisma_models.LiteLLM_OrganizationTable":
|
||||||
"""
|
"""
|
||||||
Fetch and validate an organization for team update operations.
|
Fetch and validate an organization for team update operations.
|
||||||
|
|
||||||
|
|
@ -2034,7 +2002,9 @@ async def update_team(
|
||||||
validate_budget_duration(data.budget_duration)
|
validate_budget_duration(data.budget_duration)
|
||||||
validate_budget_duration(data.team_member_budget_duration)
|
validate_budget_duration(data.team_member_budget_duration)
|
||||||
|
|
||||||
existing_team_row = await _team_db(prisma_client).find_unique(where={"team_id": data.team_id})
|
existing_team_row = await _raw_team_db(TeamRepository(prisma_client)).find_unique(
|
||||||
|
where={"team_id": data.team_id}
|
||||||
|
)
|
||||||
|
|
||||||
if existing_team_row is None:
|
if existing_team_row is None:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
@ -2272,18 +2242,16 @@ async def update_team(
|
||||||
|
|
||||||
updated_kv = prisma_client.jsonify_team_object(db_data=updated_kv)
|
updated_kv = prisma_client.jsonify_team_object(db_data=updated_kv)
|
||||||
team_update_data: Final[Mapping[str, object]] = updated_kv
|
team_update_data: Final[Mapping[str, object]] = updated_kv
|
||||||
team_row: Final[LiteLLM_TeamTable | None] = _nullable(
|
team_row: Final = await _team_db(prisma_client).update(
|
||||||
await _team_db(prisma_client).update(
|
where={"team_id": data.team_id},
|
||||||
where={"team_id": data.team_id},
|
data=team_update_data,
|
||||||
data=team_update_data,
|
# `object_permission` is included so `_refresh_cached_team`
|
||||||
# `object_permission` is included so `_refresh_cached_team`
|
# doesn't write a cached team with the relation nulled out.
|
||||||
# doesn't write a cached team with the relation nulled out —
|
# See team_model_add for the full rationale.
|
||||||
# see team_model_add for the full rationale.
|
include={
|
||||||
include={
|
"litellm_model_table": True,
|
||||||
"litellm_model_table": True,
|
"object_permission": True,
|
||||||
"object_permission": True,
|
},
|
||||||
},
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if team_row is None or team_row.team_id is None:
|
if team_row is None or team_row.team_id is None:
|
||||||
|
|
@ -2413,7 +2381,7 @@ def _set_budget_reset_at(data: UpdateTeamRequest, updated_kv: dict) -> None:
|
||||||
updated_kv["budget_limits"] = json.dumps(initialized_windows)
|
updated_kv["budget_limits"] = json.dumps(initialized_windows)
|
||||||
|
|
||||||
|
|
||||||
async def handle_update_object_permission(data_json: dict, existing_team_row: LiteLLM_TeamTable) -> dict:
|
async def handle_update_object_permission(data_json: dict, existing_team_row: _ObjectPermissionRow) -> dict:
|
||||||
"""
|
"""
|
||||||
Handle the update of object permission for a team.
|
Handle the update of object permission for a team.
|
||||||
|
|
||||||
|
|
@ -2750,7 +2718,7 @@ async def _add_team_members_to_team(
|
||||||
prisma_client: PrismaClient,
|
prisma_client: PrismaClient,
|
||||||
user_api_key_dict: UserAPIKeyAuth,
|
user_api_key_dict: UserAPIKeyAuth,
|
||||||
litellm_proxy_admin_name: str,
|
litellm_proxy_admin_name: str,
|
||||||
) -> tuple[LiteLLM_TeamTable, list[LiteLLM_UserTable], list[LiteLLM_TeamMembership]]:
|
) -> tuple["prisma_models.LiteLLM_TeamTable", list[LiteLLM_UserTable], list[LiteLLM_TeamMembership]]:
|
||||||
"""Add team members to the team, under the team's advisory lock.
|
"""Add team members to the team, under the team's advisory lock.
|
||||||
|
|
||||||
The lock (``TEAM_ADVISORY_LOCK_SQL``, keyed on the team id) is taken first, and the
|
The lock (``TEAM_ADVISORY_LOCK_SQL``, keyed on the team id) is taken first, and the
|
||||||
|
|
@ -2765,14 +2733,12 @@ async def _add_team_members_to_team(
|
||||||
a waiter that can deadlock the pool, since enough concurrent adds for one team would
|
a waiter that can deadlock the pool, since enough concurrent adds for one team would
|
||||||
hold every connection waiting on the lock while the holder waits for a free one.
|
hold every connection waiting on the lock while the holder waits for a free one.
|
||||||
"""
|
"""
|
||||||
|
gone_detail: Final[_ErrorDetail] = {"error": f"Team={data.team_id} was deleted while this member add was running"}
|
||||||
async with prisma_client.tx() as tx:
|
async with prisma_client.tx() as tx:
|
||||||
await tx.query_raw(TEAM_ADVISORY_LOCK_SQL, data.team_id)
|
await tx.query_raw(TEAM_ADVISORY_LOCK_SQL, data.team_id)
|
||||||
|
|
||||||
locked_members: Final = await TeamRepository(prisma_client).get_members_with_roles_locked(tx, data.team_id)
|
locked_members: Final = await TeamRepository(prisma_client).get_members_with_roles_locked(tx, data.team_id)
|
||||||
if locked_members is None:
|
if locked_members is None:
|
||||||
gone_detail: Final[_ErrorDetail] = {
|
|
||||||
"error": f"Team={data.team_id} was deleted while this member add was running"
|
|
||||||
}
|
|
||||||
raise HTTPException(status_code=404, detail=gone_detail)
|
raise HTTPException(status_code=404, detail=gone_detail)
|
||||||
complete_team_data.members_with_roles = locked_members
|
complete_team_data.members_with_roles = locked_members
|
||||||
|
|
||||||
|
|
@ -2792,10 +2758,12 @@ async def _add_team_members_to_team(
|
||||||
)
|
)
|
||||||
|
|
||||||
_db_team_members: Final = [m.model_dump() for m in complete_team_data.members_with_roles]
|
_db_team_members: Final = [m.model_dump() for m in complete_team_data.members_with_roles]
|
||||||
updated_team: Final = await tx.litellm_teamtable.update(
|
updated_team: Final = await _team_tx_db(tx).update(
|
||||||
where={"team_id": data.team_id},
|
where={"team_id": data.team_id},
|
||||||
data={"members_with_roles": json.dumps(_db_team_members)},
|
data={"members_with_roles": json.dumps(_db_team_members)},
|
||||||
)
|
)
|
||||||
|
if updated_team is None:
|
||||||
|
raise HTTPException(status_code=404, detail=gone_detail)
|
||||||
|
|
||||||
return updated_team, updated_users, updated_team_memberships
|
return updated_team, updated_users, updated_team_memberships
|
||||||
|
|
||||||
|
|
@ -3329,9 +3297,7 @@ async def team_member_delete(
|
||||||
{"user_id": {"in": sorted(removed_user_ids)}} if removed_user_ids else {"user_email": data.user_email}
|
{"user_id": {"in": sorted(removed_user_ids)}} if removed_user_ids else {"user_email": data.user_email}
|
||||||
)
|
)
|
||||||
member_tx: Final[_MemberDeleteTx] = tx
|
member_tx: Final[_MemberDeleteTx] = tx
|
||||||
existing_user_rows: Final[Sequence[LiteLLM_UserTable]] = await member_tx.litellm_usertable.find_many(
|
existing_user_rows: Final = await member_tx.litellm_usertable.find_many(where=key_val)
|
||||||
where=key_val
|
|
||||||
)
|
|
||||||
|
|
||||||
# Also clean up any existing team membership rows for this user and team
|
# Also clean up any existing team membership rows for this user and team
|
||||||
user_ids_to_delete: Final = removed_user_ids.union(
|
user_ids_to_delete: Final = removed_user_ids.union(
|
||||||
|
|
@ -3342,14 +3308,14 @@ async def team_member_delete(
|
||||||
## DELETE KEYS CREATED BY USER FOR THIS TEAM
|
## DELETE KEYS CREATED BY USER FOR THIS TEAM
|
||||||
# Fetch keys before deletion so their audit records can be persisted alongside the delete.
|
# Fetch keys before deletion so their audit records can be persisted alongside the delete.
|
||||||
# An empty user_ids_to_delete still resolves cleanly: prisma's "in": [] matches no rows.
|
# An empty user_ids_to_delete still resolves cleanly: prisma's "in": [] matches no rows.
|
||||||
keys_to_delete: Final[list[LiteLLM_VerificationToken]] = await member_tx.litellm_verificationtoken.find_many(
|
keys_to_delete: Final = await member_tx.litellm_verificationtoken.find_many(
|
||||||
where={
|
where={
|
||||||
"user_id": {"in": sorted(user_ids_to_delete)},
|
"user_id": {"in": sorted(user_ids_to_delete)},
|
||||||
"team_id": data.team_id,
|
"team_id": data.team_id,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
await tx.litellm_teamtable.update(
|
await _team_tx_db(tx).update(
|
||||||
where={"team_id": data.team_id},
|
where={"team_id": data.team_id},
|
||||||
data={"members_with_roles": json.dumps(_db_new_team_members)},
|
data={"members_with_roles": json.dumps(_db_new_team_members)},
|
||||||
)
|
)
|
||||||
|
|
@ -3987,9 +3953,7 @@ async def delete_team(
|
||||||
_persist_deleted_verification_tokens,
|
_persist_deleted_verification_tokens,
|
||||||
)
|
)
|
||||||
|
|
||||||
keys_to_delete: list[LiteLLM_VerificationToken] = await _tokens_db(prisma_client).find_many(
|
keys_to_delete: Final = await _tokens_db(prisma_client).find_many(where={"team_id": {"in": data.team_ids}})
|
||||||
where={"team_id": {"in": data.team_ids}}
|
|
||||||
)
|
|
||||||
|
|
||||||
if keys_to_delete:
|
if keys_to_delete:
|
||||||
await _persist_deleted_verification_tokens(
|
await _persist_deleted_verification_tokens(
|
||||||
|
|
@ -4109,7 +4073,7 @@ async def _sweep_deleted_team_references_tx(team_ids: Sequence[str], tx: _TeamDe
|
||||||
|
|
||||||
|
|
||||||
async def _invalidate_deleted_key_cache(
|
async def _invalidate_deleted_key_cache(
|
||||||
keys: Sequence[LiteLLM_VerificationToken],
|
keys: "Sequence[prisma_models.LiteLLM_VerificationToken]",
|
||||||
user_api_key_cache: UserApiKeyCache,
|
user_api_key_cache: UserApiKeyCache,
|
||||||
proxy_logging_obj: ProxyLogging,
|
proxy_logging_obj: ProxyLogging,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
@ -4294,7 +4258,7 @@ async def _hydrate_member_emails(
|
||||||
if not missing_user_ids:
|
if not missing_user_ids:
|
||||||
return tuple(members)
|
return tuple(members)
|
||||||
|
|
||||||
user_rows: Final[Sequence[LiteLLM_UserTable]] = await _user_db(prisma_client).find_many(
|
user_rows: Final[Sequence[prisma_models.LiteLLM_UserTable]] = await _user_db(prisma_client).find_many(
|
||||||
where={ # mutable-ok: Prisma query filters are dict-shaped
|
where={ # mutable-ok: Prisma query filters are dict-shaped
|
||||||
"user_id": { # mutable-ok: Prisma query filters are dict-shaped
|
"user_id": { # mutable-ok: Prisma query filters are dict-shaped
|
||||||
"in": sorted(missing_user_ids)
|
"in": sorted(missing_user_ids)
|
||||||
|
|
@ -4305,7 +4269,7 @@ async def _hydrate_member_emails(
|
||||||
|
|
||||||
return tuple(
|
return tuple(
|
||||||
m.model_copy(update={"user_email": email_by_user_id[m.user_id]}) # mutable-ok: pydantic update payload
|
m.model_copy(update={"user_email": email_by_user_id[m.user_id]}) # mutable-ok: pydantic update payload
|
||||||
if not m.user_email and m.user_id in email_by_user_id
|
if not m.user_email and m.user_id is not None and m.user_id in email_by_user_id
|
||||||
else m
|
else m
|
||||||
for m in members
|
for m in members
|
||||||
)
|
)
|
||||||
|
|
@ -4890,7 +4854,7 @@ async def _build_team_list_where_conditions(
|
||||||
|
|
||||||
async def _batch_resolve_access_group_resources(
|
async def _batch_resolve_access_group_resources(
|
||||||
all_access_group_ids: list[str],
|
all_access_group_ids: list[str],
|
||||||
) -> dict[str, LiteLLM_AccessGroupTable]:
|
) -> "dict[str, prisma_models.LiteLLM_AccessGroupTable]":
|
||||||
"""
|
"""
|
||||||
Batch-fetch access groups in a single DB query and return them keyed by
|
Batch-fetch access groups in a single DB query and return them keyed by
|
||||||
access_group_id. Missing/invalid groups are silently omitted.
|
access_group_id. Missing/invalid groups are silently omitted.
|
||||||
|
|
@ -4908,7 +4872,7 @@ async def _batch_resolve_access_group_resources(
|
||||||
|
|
||||||
|
|
||||||
def _convert_teams_to_response_models(
|
def _convert_teams_to_response_models(
|
||||||
teams: list,
|
teams: Sequence,
|
||||||
use_deleted_table: bool,
|
use_deleted_table: bool,
|
||||||
keys_count_by_team: dict[str, int] | None = None,
|
keys_count_by_team: dict[str, int] | None = None,
|
||||||
) -> list[TeamListItem | LiteLLM_TeamTable | LiteLLM_DeletedTeamTable]:
|
) -> list[TeamListItem | LiteLLM_TeamTable | LiteLLM_DeletedTeamTable]:
|
||||||
|
|
@ -4942,7 +4906,7 @@ def _convert_teams_to_response_models(
|
||||||
|
|
||||||
async def _get_keys_count_by_team(
|
async def _get_keys_count_by_team(
|
||||||
prisma_client: PrismaClient,
|
prisma_client: PrismaClient,
|
||||||
teams: Sequence[LiteLLM_TeamTable],
|
teams: Sequence[_TeamIdRow],
|
||||||
) -> dict[str, int]:
|
) -> dict[str, int]:
|
||||||
"""Aggregate virtual-key counts per team for the given page of teams.
|
"""Aggregate virtual-key counts per team for the given page of teams.
|
||||||
|
|
||||||
|
|
@ -4954,10 +4918,13 @@ async def _get_keys_count_by_team(
|
||||||
if not page_team_ids:
|
if not page_team_ids:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
grouped: Final = await _tokens_db(prisma_client).group_by(
|
grouped: Final = cast( # cast-ok: prisma group_by returns one row per `by` key with `count=` nested under "_count"
|
||||||
by=["team_id"],
|
"Sequence[_TeamIdGroupRow]",
|
||||||
where={"team_id": {"in": page_team_ids}},
|
await _tokens_db(prisma_client).group_by(
|
||||||
count={"team_id": True},
|
by=["team_id"],
|
||||||
|
where={"team_id": {"in": page_team_ids}},
|
||||||
|
count={"team_id": True},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return {row["team_id"]: row.get("_count", {}).get("team_id", 0) for row in grouped if row.get("team_id")}
|
return {row["team_id"]: row.get("_count", {}).get("team_id", 0) for row in grouped if row.get("team_id")}
|
||||||
|
|
||||||
|
|
@ -5347,7 +5314,7 @@ async def list_team(
|
||||||
_team_memberships.append(tm)
|
_team_memberships.append(tm)
|
||||||
|
|
||||||
# add all keys that belong to the team
|
# add all keys that belong to the team
|
||||||
keys = await _tokens_db(prisma_client).find_many(where={"team_id": team.team_id})
|
keys = _as_list(await _tokens_db(prisma_client).find_many(where={"team_id": team.team_id}))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
returned_responses.append(
|
returned_responses.append(
|
||||||
|
|
@ -5582,6 +5549,11 @@ async def team_model_add(
|
||||||
data={"updated_at": datetime.now(timezone.utc)},
|
data={"updated_at": datetime.now(timezone.utc)},
|
||||||
include={"object_permission": True},
|
include={"object_permission": True},
|
||||||
)
|
)
|
||||||
|
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(
|
await _refresh_cached_team(
|
||||||
team_row=updated_team,
|
team_row=updated_team,
|
||||||
|
|
@ -5664,6 +5636,11 @@ async def team_model_delete(
|
||||||
data={"models": updated_models},
|
data={"models": updated_models},
|
||||||
include={"object_permission": True},
|
include={"object_permission": True},
|
||||||
)
|
)
|
||||||
|
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(
|
await _refresh_cached_team(
|
||||||
team_row=updated_team,
|
team_row=updated_team,
|
||||||
|
|
@ -5798,8 +5775,13 @@ async def update_team_member_permissions(
|
||||||
where={"team_id": data.team_id},
|
where={"team_id": data.team_id},
|
||||||
data={"team_member_permissions": data.team_member_permissions},
|
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
|
return updated_team # pyright: ignore[reportReturnType] # prisma row, coerced by this route's response_model
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
|
|
@ -5864,7 +5846,9 @@ async def bulk_update_team_member_permissions(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def _compute_and_batch_updates(prisma_client, teams: Sequence[LiteLLM_TeamTable], permissions_to_add: set) -> int:
|
async def _compute_and_batch_updates(
|
||||||
|
prisma_client, teams: "Sequence[prisma_models.LiteLLM_TeamTable]", permissions_to_add: set
|
||||||
|
) -> int:
|
||||||
"""Compute merged permissions and batch-write updates. Returns count of teams updated."""
|
"""Compute merged permissions and batch-write updates. Returns count of teams updated."""
|
||||||
updates: Final = []
|
updates: Final = []
|
||||||
for team in teams:
|
for team in teams:
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@ from typing import (
|
||||||
NoReturn,
|
NoReturn,
|
||||||
Optional,
|
Optional,
|
||||||
Protocol,
|
Protocol,
|
||||||
TypeVar,
|
|
||||||
Union,
|
Union,
|
||||||
cast,
|
cast,
|
||||||
overload,
|
overload,
|
||||||
|
|
@ -122,6 +121,7 @@ from litellm.proxy.utils import (
|
||||||
get_custom_url,
|
get_custom_url,
|
||||||
get_server_root_path,
|
get_server_root_path,
|
||||||
)
|
)
|
||||||
|
from litellm.repositories.prisma_protocols import TableActions
|
||||||
from litellm.repositories.table_repositories import SSOConfigRepository
|
from litellm.repositories.table_repositories import SSOConfigRepository
|
||||||
from litellm.repositories.team_repository import TeamRepository
|
from litellm.repositories.team_repository import TeamRepository
|
||||||
from litellm.repositories.user_repository import UserRepository
|
from litellm.repositories.user_repository import UserRepository
|
||||||
|
|
@ -171,51 +171,16 @@ _CLI_SSO_SECRET_KEY_FRAGMENTS: Final = frozenset(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
_DbRecordT: Final = TypeVar("_DbRecordT", covariant=True)
|
|
||||||
|
|
||||||
|
|
||||||
class _PrismaTableActions(Protocol[_DbRecordT]):
|
|
||||||
async def find_unique(
|
|
||||||
self,
|
|
||||||
where: Mapping[str, object],
|
|
||||||
) -> _DbRecordT | None: ...
|
|
||||||
|
|
||||||
async def find_first(
|
|
||||||
self,
|
|
||||||
where: Mapping[str, object] | None = None,
|
|
||||||
) -> _DbRecordT | None: ...
|
|
||||||
|
|
||||||
async def find_many(
|
|
||||||
self,
|
|
||||||
where: Mapping[str, object] | None = None,
|
|
||||||
include: Mapping[str, bool] | None = None,
|
|
||||||
) -> Sequence[_DbRecordT]: ...
|
|
||||||
|
|
||||||
async def update(
|
|
||||||
self,
|
|
||||||
where: Mapping[str, object],
|
|
||||||
data: Mapping[str, object],
|
|
||||||
) -> _DbRecordT: ...
|
|
||||||
|
|
||||||
async def update_many(
|
|
||||||
self,
|
|
||||||
where: Mapping[str, object],
|
|
||||||
data: Mapping[str, object],
|
|
||||||
) -> int: ...
|
|
||||||
|
|
||||||
|
|
||||||
class _UserMetadataRow(Protocol):
|
class _UserMetadataRow(Protocol):
|
||||||
@property
|
@property
|
||||||
def metadata(self) -> Mapping[str, object] | None: ...
|
def metadata(self) -> Mapping[str, object] | None: ...
|
||||||
|
|
||||||
|
|
||||||
class _HasUserMetadataTable(Protocol):
|
def _user_meta_db(repo: UserRepository) -> "TableActions[_UserMetadataRow]":
|
||||||
@property
|
return cast( # cast-ok: prisma types Json columns as str; the client hands back the deserialized value
|
||||||
def table(self) -> "_PrismaTableActions[_UserMetadataRow]": ...
|
"TableActions[_UserMetadataRow]", repo.table
|
||||||
|
)
|
||||||
|
|
||||||
def _user_meta_db(repo: "_HasUserMetadataTable") -> "_PrismaTableActions[_UserMetadataRow]":
|
|
||||||
return repo.table
|
|
||||||
|
|
||||||
|
|
||||||
class _SsoConfigRow(Protocol):
|
class _SsoConfigRow(Protocol):
|
||||||
|
|
@ -223,25 +188,17 @@ class _SsoConfigRow(Protocol):
|
||||||
def sso_settings(self) -> Mapping[str, object] | None: ...
|
def sso_settings(self) -> Mapping[str, object] | None: ...
|
||||||
|
|
||||||
|
|
||||||
class _HasSsoConfigTable(Protocol):
|
def _sso_config_db(repo: SSOConfigRepository) -> "TableActions[_SsoConfigRow]":
|
||||||
@property
|
return cast( # cast-ok: prisma types Json columns as str; the client hands back the deserialized value
|
||||||
def table(self) -> "_PrismaTableActions[_SsoConfigRow]": ...
|
"TableActions[_SsoConfigRow]", repo.table
|
||||||
|
)
|
||||||
|
|
||||||
def _sso_config_db(repo: "_HasSsoConfigTable") -> "_PrismaTableActions[_SsoConfigRow]":
|
|
||||||
return repo.table
|
|
||||||
|
|
||||||
|
|
||||||
class _TeamDetailRow(Protocol):
|
class _TeamDetailRow(Protocol):
|
||||||
def model_dump(self) -> Mapping[str, object]: ...
|
def model_dump(self) -> Mapping[str, object]: ...
|
||||||
|
|
||||||
|
|
||||||
class _HasTeamDetailTable(Protocol):
|
def _team_detail_db(repo: TeamRepository) -> "TableActions[_TeamDetailRow]":
|
||||||
@property
|
|
||||||
def table(self) -> "_PrismaTableActions[_TeamDetailRow]": ...
|
|
||||||
|
|
||||||
|
|
||||||
def _team_detail_db(repo: "_HasTeamDetailTable") -> "_PrismaTableActions[_TeamDetailRow]":
|
|
||||||
return repo.table
|
return repo.table
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ organizations, teams, and keys.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping, Sequence
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING, Any, Final, Optional
|
from typing import TYPE_CHECKING, Any, Final, Optional
|
||||||
|
|
||||||
|
|
@ -19,6 +19,8 @@ from litellm.repositories.object_permission_repository import ObjectPermissionRe
|
||||||
from litellm.repositories.table_repositories import MCPServerRepository
|
from litellm.repositories.table_repositories import MCPServerRepository
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from prisma import models as prisma_models
|
||||||
|
|
||||||
from litellm.proxy._types import (
|
from litellm.proxy._types import (
|
||||||
LiteLLM_ObjectPermissionTable,
|
LiteLLM_ObjectPermissionTable,
|
||||||
LiteLLM_TeamTableCachedObj,
|
LiteLLM_TeamTableCachedObj,
|
||||||
|
|
@ -26,7 +28,7 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
|
|
||||||
async def attach_object_permission_to_dict(
|
async def attach_object_permission_to_dict(
|
||||||
data_dict: dict,
|
data_dict: dict[str, object],
|
||||||
prisma_client: PrismaClient,
|
prisma_client: PrismaClient,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
|
|
@ -61,7 +63,7 @@ async def attach_object_permission_to_dict(
|
||||||
try:
|
try:
|
||||||
object_permission = object_permission.model_dump()
|
object_permission = object_permission.model_dump()
|
||||||
except Exception:
|
except Exception:
|
||||||
object_permission = object_permission.dict()
|
object_permission = object_permission.dict() # pyright: ignore[reportDeprecated] # pydantic v1 fallback
|
||||||
data_dict["object_permission"] = object_permission
|
data_dict["object_permission"] = object_permission
|
||||||
return data_dict
|
return data_dict
|
||||||
|
|
||||||
|
|
@ -188,7 +190,9 @@ async def _set_object_permission(
|
||||||
return data_json
|
return data_json
|
||||||
|
|
||||||
# Clean data: exclude None values and object_permission_id
|
# Clean data: exclude None values and object_permission_id
|
||||||
clean_data: Final = {k: v for k, v in permission_data.items() if v is not None and k != "object_permission_id"}
|
clean_data: Final[dict[str, object]] = {
|
||||||
|
k: v for k, v in permission_data.items() if v is not None and k != "object_permission_id"
|
||||||
|
}
|
||||||
|
|
||||||
# Serialize mcp_tool_permissions to JSON string for GraphQL compatibility
|
# Serialize mcp_tool_permissions to JSON string for GraphQL compatibility
|
||||||
if "mcp_tool_permissions" in clean_data:
|
if "mcp_tool_permissions" in clean_data:
|
||||||
|
|
@ -224,7 +228,7 @@ def _mcp_server_identifier_matches(server: Any, identifier: str) -> bool:
|
||||||
async def _get_db_mcp_servers_by_identifiers(
|
async def _get_db_mcp_servers_by_identifiers(
|
||||||
identifiers: set[str],
|
identifiers: set[str],
|
||||||
prisma_client: PrismaClient | None,
|
prisma_client: PrismaClient | None,
|
||||||
) -> list[Any]:
|
) -> "Sequence[prisma_models.LiteLLM_MCPServerTable]":
|
||||||
if prisma_client is None or not identifiers:
|
if prisma_client is None or not identifiers:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,21 +18,20 @@ Scoping:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from collections.abc import Mapping, Sequence
|
from collections.abc import Mapping
|
||||||
from datetime import datetime
|
from typing import TYPE_CHECKING, Final
|
||||||
from typing import TYPE_CHECKING, Final, Protocol
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
|
|
||||||
from litellm._logging import verbose_proxy_logger
|
from litellm._logging import verbose_proxy_logger
|
||||||
from litellm.proxy._types import (
|
from litellm.proxy._types import (
|
||||||
CommonProxyErrors,
|
CommonProxyErrors,
|
||||||
LiteLLM_TeamTable,
|
|
||||||
LitellmUserRoles,
|
LitellmUserRoles,
|
||||||
UserAPIKeyAuth,
|
UserAPIKeyAuth,
|
||||||
user_api_key_has_admin_view,
|
user_api_key_has_admin_view,
|
||||||
)
|
)
|
||||||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||||
|
from litellm.repositories.prisma_protocols import TableActions
|
||||||
from litellm.repositories.table_repositories import MemoryRepository
|
from litellm.repositories.table_repositories import MemoryRepository
|
||||||
from litellm.repositories.team_repository import TeamRepository
|
from litellm.repositories.team_repository import TeamRepository
|
||||||
from litellm.types.memory_management import (
|
from litellm.types.memory_management import (
|
||||||
|
|
@ -44,54 +43,17 @@ from litellm.types.memory_management import (
|
||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from prisma import models as prisma_models
|
||||||
|
|
||||||
from litellm.proxy.utils import PrismaClient
|
from litellm.proxy.utils import PrismaClient
|
||||||
|
|
||||||
router: Final = APIRouter()
|
router: Final = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
class _MemoryRecord(Protocol):
|
def _memory_table(prisma_client: "PrismaClient") -> TableActions["prisma_models.LiteLLM_MemoryTable"]:
|
||||||
memory_id: str
|
|
||||||
key: str
|
|
||||||
value: str
|
|
||||||
metadata: object
|
|
||||||
user_id: str | None
|
|
||||||
team_id: str | None
|
|
||||||
created_at: datetime | None
|
|
||||||
created_by: str | None
|
|
||||||
updated_at: datetime | None
|
|
||||||
updated_by: str | None
|
|
||||||
|
|
||||||
|
|
||||||
class _MemoryTableActions(Protocol):
|
|
||||||
async def create(self, data: Mapping[str, object]) -> _MemoryRecord: ...
|
|
||||||
|
|
||||||
async def find_many(
|
|
||||||
self,
|
|
||||||
where: Mapping[str, object] | None = ...,
|
|
||||||
order: Mapping[str, str] | None = ...,
|
|
||||||
skip: int = ...,
|
|
||||||
take: int = ...,
|
|
||||||
) -> Sequence[_MemoryRecord]: ...
|
|
||||||
|
|
||||||
async def count(self, where: Mapping[str, object] | None = ...) -> int: ...
|
|
||||||
|
|
||||||
async def update(self, where: Mapping[str, object], data: Mapping[str, object]) -> _MemoryRecord: ...
|
|
||||||
|
|
||||||
async def delete(self, where: Mapping[str, object]) -> _MemoryRecord | None: ...
|
|
||||||
|
|
||||||
|
|
||||||
def _memory_table(prisma_client: "PrismaClient") -> _MemoryTableActions:
|
|
||||||
return MemoryRepository(prisma_client).table
|
return MemoryRepository(prisma_client).table
|
||||||
|
|
||||||
|
|
||||||
class _TeamTableActions(Protocol):
|
|
||||||
async def find_unique(self, where: Mapping[str, str]) -> LiteLLM_TeamTable | None: ...
|
|
||||||
|
|
||||||
|
|
||||||
def _team_table(prisma_client: "PrismaClient") -> _TeamTableActions:
|
|
||||||
return TeamRepository(prisma_client).table
|
|
||||||
|
|
||||||
|
|
||||||
def _serialize_metadata_for_prisma(metadata: object) -> str:
|
def _serialize_metadata_for_prisma(metadata: object) -> str:
|
||||||
"""
|
"""
|
||||||
Encode a `metadata` payload for the `Json?` column.
|
Encode a `metadata` payload for the `Json?` column.
|
||||||
|
|
@ -129,7 +91,7 @@ def _visibility_filter(user_api_key_dict: UserAPIKeyAuth) -> Mapping[str, object
|
||||||
return {"OR": ors}
|
return {"OR": ors}
|
||||||
|
|
||||||
|
|
||||||
def _row_to_model(row: _MemoryRecord) -> LiteLLM_MemoryRow:
|
def _row_to_model(row: "prisma_models.LiteLLM_MemoryTable") -> LiteLLM_MemoryRow:
|
||||||
return LiteLLM_MemoryRow(
|
return LiteLLM_MemoryRow(
|
||||||
memory_id=row.memory_id,
|
memory_id=row.memory_id,
|
||||||
key=row.key,
|
key=row.key,
|
||||||
|
|
@ -163,7 +125,7 @@ def _internal_error(log_message: str, exc: Exception, default_detail: str) -> HT
|
||||||
|
|
||||||
|
|
||||||
async def _assert_write_access(
|
async def _assert_write_access(
|
||||||
prisma_client: "PrismaClient", row: _MemoryRecord, user_api_key_dict: UserAPIKeyAuth
|
prisma_client: "PrismaClient", row: "prisma_models.LiteLLM_MemoryTable", user_api_key_dict: UserAPIKeyAuth
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Enforce ownership for mutations (PUT/DELETE).
|
Enforce ownership for mutations (PUT/DELETE).
|
||||||
|
|
@ -219,7 +181,7 @@ async def _is_team_admin_for(prisma_client: "PrismaClient", user_api_key_dict: U
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
team_obj: Final = await _team_table(prisma_client).find_unique(where={"team_id": team_id})
|
team_obj: Final = await TeamRepository(prisma_client).find_by_id(team_id, id_field="team_id")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
verbose_proxy_logger.exception("Error loading team for write-auth check (team_id=%s): %s", team_id, e)
|
verbose_proxy_logger.exception("Error loading team for write-auth check (team_id=%s): %s", team_id, e)
|
||||||
return False
|
return False
|
||||||
|
|
@ -407,7 +369,7 @@ async def list_memory(
|
||||||
|
|
||||||
async def _find_memory_for_caller(
|
async def _find_memory_for_caller(
|
||||||
prisma_client: "PrismaClient", key: str, user_api_key_dict: UserAPIKeyAuth
|
prisma_client: "PrismaClient", key: str, user_api_key_dict: UserAPIKeyAuth
|
||||||
) -> _MemoryRecord:
|
) -> "prisma_models.LiteLLM_MemoryTable":
|
||||||
"""Look up a memory row by key, scoped to the caller's visibility."""
|
"""Look up a memory row by key, scoped to the caller's visibility."""
|
||||||
key_filter: Final[Mapping[str, object]] = {"key": key}
|
key_filter: Final[Mapping[str, object]] = {"key": key}
|
||||||
vis: Final = _visibility_filter(user_api_key_dict)
|
vis: Final = _visibility_filter(user_api_key_dict)
|
||||||
|
|
@ -418,6 +380,18 @@ async def _find_memory_for_caller(
|
||||||
return rows[0]
|
return rows[0]
|
||||||
|
|
||||||
|
|
||||||
|
async def _find_visible_memory_or_none(
|
||||||
|
prisma_client: "PrismaClient", key: str, user_api_key_dict: UserAPIKeyAuth
|
||||||
|
) -> "prisma_models.LiteLLM_MemoryTable | None":
|
||||||
|
"""The caller-visible row for `key`, or None when nothing is visible to them."""
|
||||||
|
try:
|
||||||
|
return await _find_memory_for_caller(prisma_client, key, user_api_key_dict)
|
||||||
|
except HTTPException as e:
|
||||||
|
if e.status_code == 404:
|
||||||
|
return None
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/v1/memory/{key:path}",
|
"/v1/memory/{key:path}",
|
||||||
tags=["memory management"],
|
tags=["memory management"],
|
||||||
|
|
@ -480,17 +454,8 @@ async def upsert_memory(
|
||||||
)
|
)
|
||||||
data["updated_by"] = user_api_key_dict.user_id
|
data["updated_by"] = user_api_key_dict.user_id
|
||||||
|
|
||||||
async def _find_existing() -> _MemoryRecord | None:
|
|
||||||
"""Return the caller-visible row for `key`, or None."""
|
|
||||||
try:
|
|
||||||
return await _find_memory_for_caller(prisma_client, key, user_api_key_dict)
|
|
||||||
except HTTPException as e:
|
|
||||||
if e.status_code == 404:
|
|
||||||
return None
|
|
||||||
raise
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
existing: Final = await _find_existing()
|
existing: Final = await _find_visible_memory_or_none(prisma_client, key, user_api_key_dict)
|
||||||
if existing is not None:
|
if existing is not None:
|
||||||
# Visibility != write authority. Make sure the caller actually
|
# Visibility != write authority. Make sure the caller actually
|
||||||
# owns this row (their user_id matches, or it's a pure team row in
|
# owns this row (their user_id matches, or it's a pure team row in
|
||||||
|
|
@ -530,7 +495,7 @@ async def upsert_memory(
|
||||||
# instead of surfacing a 500 on a unique-violation.
|
# instead of surfacing a 500 on a unique-violation.
|
||||||
if not _is_unique_violation(e):
|
if not _is_unique_violation(e):
|
||||||
raise
|
raise
|
||||||
existing_after_race: Final = await _find_existing()
|
existing_after_race: Final = await _find_visible_memory_or_none(prisma_client, key, user_api_key_dict)
|
||||||
if existing_after_race is None:
|
if existing_after_race is None:
|
||||||
# Row exists globally but isn't visible to this caller
|
# Row exists globally but isn't visible to this caller
|
||||||
# (owned by someone else). Treat as conflict.
|
# (owned by someone else). Treat as conflict.
|
||||||
|
|
@ -549,6 +514,8 @@ async def upsert_memory(
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise _internal_error("Error upserting memory: %s", e, "Internal error updating memory entry.")
|
raise _internal_error("Error upserting memory: %s", e, "Internal error updating memory entry.")
|
||||||
|
|
||||||
|
if row is None:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Memory with key '{key}' not found")
|
||||||
return _row_to_model(row)
|
return _row_to_model(row)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -568,8 +535,10 @@ async def delete_memory(
|
||||||
# Visibility != write authority — see the upsert handler for the rationale.
|
# Visibility != write authority — see the upsert handler for the rationale.
|
||||||
await _assert_write_access(prisma_client, row, user_api_key_dict)
|
await _assert_write_access(prisma_client, row, user_api_key_dict)
|
||||||
try:
|
try:
|
||||||
await _memory_table(prisma_client).delete(where={"memory_id": row.memory_id})
|
deleted: Final = await _memory_table(prisma_client).delete(where={"memory_id": row.memory_id})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise _internal_error("Error deleting memory: %s", e, "Internal error deleting memory entry.")
|
raise _internal_error("Error deleting memory: %s", e, "Internal error deleting memory entry.")
|
||||||
|
|
||||||
|
if deleted is None:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Memory with key '{key}' not found")
|
||||||
return MemoryDeleteResponse(key=key, deleted=True)
|
return MemoryDeleteResponse(key=key, deleted=True)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,16 @@ import re
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from types import MappingProxyType
|
from types import MappingProxyType
|
||||||
from typing import TYPE_CHECKING, Final, Literal, Optional, Protocol, get_args, runtime_checkable
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Final,
|
||||||
|
Literal,
|
||||||
|
Optional,
|
||||||
|
Protocol,
|
||||||
|
cast, # noqa: TID251 # prisma types Json columns as fields.Json but de-serializes them to plain python on read
|
||||||
|
get_args,
|
||||||
|
runtime_checkable,
|
||||||
|
)
|
||||||
|
|
||||||
from litellm.proxy._types import ProxyException
|
from litellm.proxy._types import ProxyException
|
||||||
from litellm.repositories.table_repositories import (
|
from litellm.repositories.table_repositories import (
|
||||||
|
|
@ -1183,7 +1192,7 @@ async def ensure_batch_response_managed_file_ids(
|
||||||
prisma_client,
|
prisma_client,
|
||||||
verbose_proxy_logger,
|
verbose_proxy_logger,
|
||||||
user_api_key_dict=None,
|
user_api_key_dict=None,
|
||||||
db_batch_object=None,
|
db_batch_object: "LiteLLM_ManagedObjectTable | None" = None,
|
||||||
unified_batch_id: str | Literal[False] | None = None,
|
unified_batch_id: str | Literal[False] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Normalize batch file IDs to managed unified IDs before DB persistence."""
|
"""Normalize batch file IDs to managed unified IDs before DB persistence."""
|
||||||
|
|
@ -1270,11 +1279,10 @@ async def get_batch_from_database(
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
# Parse the batch object from database
|
# Parse the batch object from database
|
||||||
batch_data: Final = (
|
file_object: Final = cast( # cast-ok: prisma types the Json column as str; reads return the decoded value
|
||||||
json.loads(db_batch_object.file_object)
|
"Mapping[str, object] | str", db_batch_object.file_object
|
||||||
if isinstance(db_batch_object.file_object, str)
|
|
||||||
else db_batch_object.file_object
|
|
||||||
)
|
)
|
||||||
|
batch_data: Final = json.loads(file_object) if isinstance(file_object, str) else file_object
|
||||||
response: Final = LiteLLMBatch.model_validate(batch_data)
|
response: Final = LiteLLMBatch.model_validate(batch_data)
|
||||||
response.id = batch_id
|
response.id = batch_id
|
||||||
|
|
||||||
|
|
@ -1360,7 +1368,7 @@ async def update_batch_in_database(
|
||||||
managed_files_obj,
|
managed_files_obj,
|
||||||
prisma_client,
|
prisma_client,
|
||||||
verbose_proxy_logger,
|
verbose_proxy_logger,
|
||||||
db_batch_object=None,
|
db_batch_object: "LiteLLM_ManagedObjectTable | None" = None,
|
||||||
operation: str = "update",
|
operation: str = "update",
|
||||||
user_api_key_dict=None,
|
user_api_key_dict=None,
|
||||||
poller_owns_accounting: bool | None = None,
|
poller_owns_accounting: bool | None = None,
|
||||||
|
|
@ -1427,7 +1435,7 @@ async def update_batch_in_database(
|
||||||
# Normalize status for database storage
|
# Normalize status for database storage
|
||||||
db_status: Final = response.status if response.status != "completed" else "complete"
|
db_status: Final = response.status if response.status != "completed" else "complete"
|
||||||
|
|
||||||
update_data: Final[dict] = {
|
update_data: Final[dict[str, object]] = {
|
||||||
"status": db_status,
|
"status": db_status,
|
||||||
"file_object": response.model_dump_json(),
|
"file_object": response.model_dump_json(),
|
||||||
"updated_at": litellm.utils.get_utc_datetime(),
|
"updated_at": litellm.utils.get_utc_datetime(),
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,13 @@ from __future__ import annotations
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from collections.abc import Callable, Mapping, Sequence
|
from collections.abc import Callable, Mapping, Sequence
|
||||||
from typing import TYPE_CHECKING, Final, TypeVar, overload
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Final,
|
||||||
|
TypeVar,
|
||||||
|
cast, # noqa: TID251 # prisma stubs type Json columns as fields.Json but de-serialize them on read
|
||||||
|
overload,
|
||||||
|
)
|
||||||
from urllib.parse import quote, unquote
|
from urllib.parse import quote, unquote
|
||||||
|
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
|
|
@ -286,11 +292,15 @@ def _canonical_path(route: str) -> str:
|
||||||
|
|
||||||
|
|
||||||
def _file_table(prisma_client: PrismaClient) -> ManagedFileTable:
|
def _file_table(prisma_client: PrismaClient) -> ManagedFileTable:
|
||||||
return ManagedFileRepository(prisma_client).table
|
return cast( # cast-ok: stub-only mismatch, prisma returns real lists and de-serialized Json
|
||||||
|
ManagedFileTable, ManagedFileRepository(prisma_client).table
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _object_table(prisma_client: PrismaClient) -> ManagedObjectTable:
|
def _object_table(prisma_client: PrismaClient) -> ManagedObjectTable:
|
||||||
return ManagedObjectRepository(prisma_client).table
|
return cast( # cast-ok: stub-only mismatch, prisma returns real lists and de-serialized Json
|
||||||
|
ManagedObjectTable, ManagedObjectRepository(prisma_client).table
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def _resolve_one(
|
async def _resolve_one(
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import json
|
||||||
import posixpath
|
import posixpath
|
||||||
import traceback
|
import traceback
|
||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
from collections.abc import AsyncGenerator, Callable, Iterable, Mapping
|
from collections.abc import AsyncGenerator, Callable, Iterable, Mapping, Sequence
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
from typing import Any, Final, TypedDict, cast
|
from typing import Any, Final, TypedDict, cast
|
||||||
|
|
@ -3183,13 +3183,18 @@ async def _filter_endpoints_by_team_allowed_routes(
|
||||||
)
|
)
|
||||||
|
|
||||||
# retrieve team metadata
|
# retrieve team metadata
|
||||||
team_metadata: Final = team.metadata
|
team_metadata: Final = cast( # cast-ok: prisma types the Json column as str; reads hand back the decoded value
|
||||||
|
"Mapping[str, object] | None", team.metadata
|
||||||
|
)
|
||||||
if team_metadata is not None and team_metadata.get("allowed_passthrough_routes") is not None:
|
if team_metadata is not None and team_metadata.get("allowed_passthrough_routes") is not None:
|
||||||
## FILTER pass_through_endpoints by allowed_passthrough_routes
|
## FILTER pass_through_endpoints by allowed_passthrough_routes
|
||||||
pass_through_endpoints = [
|
pass_through_endpoints = [
|
||||||
endpoint
|
endpoint
|
||||||
for endpoint in pass_through_endpoints
|
for endpoint in pass_through_endpoints
|
||||||
if endpoint.path in team_metadata.get("allowed_passthrough_routes")
|
if endpoint.path
|
||||||
|
in cast( # cast-ok: guarded above; team metadata stores this key as a list of route paths
|
||||||
|
"Sequence[str]", team_metadata.get("allowed_passthrough_routes")
|
||||||
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
return pass_through_endpoints
|
return pass_through_endpoints
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,20 @@ by policy_attachments (see AttachmentRegistry).
|
||||||
import json
|
import json
|
||||||
from collections.abc import Mapping, Sequence
|
from collections.abc import Mapping, Sequence
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import TYPE_CHECKING, Any, Final, Literal, Optional, Protocol, TypedDict, Union
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Any,
|
||||||
|
Final,
|
||||||
|
Literal,
|
||||||
|
Optional,
|
||||||
|
Protocol,
|
||||||
|
TypedDict,
|
||||||
|
Union,
|
||||||
|
cast, # noqa: TID251 # prisma types the condition/pipeline Json columns as str, but reads return decoded values
|
||||||
|
)
|
||||||
|
|
||||||
from litellm._logging import verbose_proxy_logger
|
from litellm._logging import verbose_proxy_logger
|
||||||
|
from litellm.repositories.prisma_protocols import TableActions
|
||||||
from litellm.repositories.table_repositories import PolicyRepository
|
from litellm.repositories.table_repositories import PolicyRepository
|
||||||
from litellm.types.proxy.policy_engine import (
|
from litellm.types.proxy.policy_engine import (
|
||||||
GuardrailPipeline,
|
GuardrailPipeline,
|
||||||
|
|
@ -65,15 +76,32 @@ class _PolicyRow(Protocol):
|
||||||
|
|
||||||
|
|
||||||
class _PolicyVersionSourceRow(Protocol):
|
class _PolicyVersionSourceRow(Protocol):
|
||||||
policy_id: str
|
@property
|
||||||
policy_name: str
|
def policy_id(self) -> str: ...
|
||||||
version_number: int
|
|
||||||
inherit: str | None
|
@property
|
||||||
description: str | None
|
def policy_name(self) -> str: ...
|
||||||
guardrails_add: Sequence[str] | None
|
|
||||||
guardrails_remove: Sequence[str] | None
|
@property
|
||||||
condition: Mapping[str, object] | str | None
|
def version_number(self) -> int: ...
|
||||||
pipeline: Mapping[str, object] | str | None
|
|
||||||
|
@property
|
||||||
|
def inherit(self) -> str | None: ...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def description(self) -> str | None: ...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def guardrails_add(self) -> Sequence[str] | None: ...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def guardrails_remove(self) -> Sequence[str] | None: ...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def condition(self) -> Mapping[str, object] | str | None: ...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pipeline(self) -> Mapping[str, object] | str | None: ...
|
||||||
|
|
||||||
|
|
||||||
class _PolicyTableClient(Protocol):
|
class _PolicyTableClient(Protocol):
|
||||||
|
|
@ -96,23 +124,15 @@ class _PolicyTableClient(Protocol):
|
||||||
async def delete_many(self, where: Mapping[str, object]) -> int: ...
|
async def delete_many(self, where: Mapping[str, object]) -> int: ...
|
||||||
|
|
||||||
|
|
||||||
class _PolicyVersionSourceTableClient(Protocol):
|
|
||||||
async def find_unique(self, where: Mapping[str, object]) -> _PolicyVersionSourceRow | None: ...
|
|
||||||
|
|
||||||
async def find_first(
|
|
||||||
self,
|
|
||||||
where: Mapping[str, object],
|
|
||||||
order: Mapping[str, str] | None = None,
|
|
||||||
) -> _PolicyVersionSourceRow | None: ...
|
|
||||||
|
|
||||||
|
|
||||||
def _policy_table(prisma_client: "PrismaClient") -> _PolicyTableClient:
|
def _policy_table(prisma_client: "PrismaClient") -> _PolicyTableClient:
|
||||||
table: Final[_PolicyTableClient] = PolicyRepository(prisma_client).table
|
table: Final = PolicyRepository(prisma_client).table
|
||||||
return table
|
return cast( # cast-ok: prisma types Json columns as str; the client hands back the decoded condition/pipeline
|
||||||
|
"_PolicyTableClient", table
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _policy_version_source_table(prisma_client: "PrismaClient") -> _PolicyVersionSourceTableClient:
|
def _policy_version_source_table(prisma_client: "PrismaClient") -> "TableActions[_PolicyVersionSourceRow]":
|
||||||
table: Final[_PolicyVersionSourceTableClient] = PolicyRepository(prisma_client).table
|
table: Final[TableActions[_PolicyVersionSourceRow]] = PolicyRepository(prisma_client).table
|
||||||
return table
|
return table
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@ Policy resolve and attachment impact estimation endpoints.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from typing import Final
|
from collections.abc import Sequence
|
||||||
|
from typing import TYPE_CHECKING, Final
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
|
|
||||||
|
|
@ -30,25 +31,28 @@ from litellm.types.proxy.policy_engine import (
|
||||||
PolicyResolveResponse,
|
PolicyResolveResponse,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from prisma import models as prisma_models
|
||||||
|
|
||||||
router: Final = APIRouter()
|
router: Final = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
def _build_alias_where(field: str, patterns: list) -> dict:
|
def _build_alias_where(field: str, patterns: Sequence[str]) -> dict[str, object]:
|
||||||
"""Build a Prisma ``where`` clause for alias patterns.
|
"""Build a Prisma ``where`` clause for alias patterns.
|
||||||
|
|
||||||
Supports exact matches and suffix wildcards (``prefix*``).
|
Supports exact matches and suffix wildcards (``prefix*``).
|
||||||
Returns something like:
|
Returns something like:
|
||||||
{"OR": [{"field": {"in": ["a","b"]}}, {"field": {"startsWith": "dev-"}}]}
|
{"OR": [{"field": {"in": ["a","b"]}}, {"field": {"startsWith": "dev-"}}]}
|
||||||
"""
|
"""
|
||||||
exact: Final[list] = []
|
exact: Final[list[str]] = []
|
||||||
prefix_conditions: Final[list] = []
|
prefix_conditions: Final[list[dict[str, object]]] = []
|
||||||
for pat in patterns:
|
for pat in patterns:
|
||||||
if pat.endswith("*"):
|
if pat.endswith("*"):
|
||||||
prefix_conditions.append({field: {"startsWith": pat[:-1]}})
|
prefix_conditions.append({field: {"startsWith": pat[:-1]}})
|
||||||
else:
|
else:
|
||||||
exact.append(pat)
|
exact.append(pat)
|
||||||
|
|
||||||
conditions: Final[list] = []
|
conditions: Final[list[dict[str, object]]] = []
|
||||||
if exact:
|
if exact:
|
||||||
conditions.append({field: {"in": exact}})
|
conditions.append({field: {"in": exact}})
|
||||||
conditions.extend(prefix_conditions)
|
conditions.extend(prefix_conditions)
|
||||||
|
|
@ -79,7 +83,7 @@ def _get_tags_from_metadata(metadata: object, json_metadata: object = None) -> l
|
||||||
return parsed.get("tags", []) or []
|
return parsed.get("tags", []) or []
|
||||||
|
|
||||||
|
|
||||||
async def _fetch_all_teams(prisma_client: object) -> list:
|
async def _fetch_all_teams(prisma_client: object) -> "Sequence[prisma_models.LiteLLM_TeamTable]":
|
||||||
"""Fetch teams from DB once. Reuse the result across tag and alias lookups."""
|
"""Fetch teams from DB once. Reuse the result across tag and alias lookups."""
|
||||||
return await TeamRepository(prisma_client).table.find_many(
|
return await TeamRepository(prisma_client).table.find_many(
|
||||||
where={},
|
where={},
|
||||||
|
|
@ -88,13 +92,15 @@ async def _fetch_all_teams(prisma_client: object) -> list:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _filter_keys_by_tags(keys: list, tag_patterns: list) -> tuple:
|
def _filter_keys_by_tags(
|
||||||
|
keys: "Sequence[prisma_models.LiteLLM_VerificationToken]", tag_patterns: Sequence[str]
|
||||||
|
) -> tuple[list[str], int]:
|
||||||
"""Filter key rows whose metadata.tags match any of the given patterns.
|
"""Filter key rows whose metadata.tags match any of the given patterns.
|
||||||
|
|
||||||
Returns (named_aliases, unnamed_count).
|
Returns (named_aliases, unnamed_count).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
affected: Final[list] = []
|
affected: Final[list[str]] = []
|
||||||
unnamed_count = 0
|
unnamed_count = 0
|
||||||
for key in keys:
|
for key in keys:
|
||||||
key_alias = key.key_alias or ""
|
key_alias = key.key_alias or ""
|
||||||
|
|
@ -111,13 +117,15 @@ def _filter_keys_by_tags(keys: list, tag_patterns: list) -> tuple:
|
||||||
return affected, unnamed_count
|
return affected, unnamed_count
|
||||||
|
|
||||||
|
|
||||||
def _filter_teams_by_tags(teams: list, tag_patterns: list) -> tuple:
|
def _filter_teams_by_tags(
|
||||||
|
teams: "Sequence[prisma_models.LiteLLM_TeamTable]", tag_patterns: Sequence[str]
|
||||||
|
) -> tuple[list[str], int]:
|
||||||
"""Filter pre-fetched team rows whose metadata.tags match any patterns.
|
"""Filter pre-fetched team rows whose metadata.tags match any patterns.
|
||||||
|
|
||||||
Returns (named_aliases, unnamed_count).
|
Returns (named_aliases, unnamed_count).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
affected: Final[list] = []
|
affected: Final[list[str]] = []
|
||||||
unnamed_count = 0
|
unnamed_count = 0
|
||||||
for team in teams:
|
for team in teams:
|
||||||
team_alias = team.team_alias or ""
|
team_alias = team.team_alias or ""
|
||||||
|
|
@ -136,18 +144,18 @@ def _filter_teams_by_tags(teams: list, tag_patterns: list) -> tuple:
|
||||||
|
|
||||||
async def _find_affected_by_team_patterns(
|
async def _find_affected_by_team_patterns(
|
||||||
prisma_client: object,
|
prisma_client: object,
|
||||||
all_teams: list,
|
all_teams: "Sequence[prisma_models.LiteLLM_TeamTable]",
|
||||||
team_patterns: list,
|
team_patterns: Sequence[str],
|
||||||
existing_teams: list,
|
existing_teams: Sequence[str],
|
||||||
existing_keys: list,
|
existing_keys: Sequence[str],
|
||||||
) -> tuple:
|
) -> tuple[list[str], list[str], int]:
|
||||||
"""Filter pre-fetched teams by alias patterns, then fetch their keys.
|
"""Filter pre-fetched teams by alias patterns, then fetch their keys.
|
||||||
|
|
||||||
Returns (new_teams, new_keys, unnamed_keys_count).
|
Returns (new_teams, new_keys, unnamed_keys_count).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
new_teams: Final[list] = []
|
new_teams: Final[list[str]] = []
|
||||||
matched_team_ids: Final[list] = []
|
matched_team_ids: Final[list[str]] = []
|
||||||
|
|
||||||
for team in all_teams:
|
for team in all_teams:
|
||||||
team_alias = team.team_alias or ""
|
team_alias = team.team_alias or ""
|
||||||
|
|
@ -158,7 +166,7 @@ async def _find_affected_by_team_patterns(
|
||||||
new_teams.append(team_alias)
|
new_teams.append(team_alias)
|
||||||
matched_team_ids.append(str(team.team_id))
|
matched_team_ids.append(str(team.team_id))
|
||||||
|
|
||||||
new_keys: Final[list] = []
|
new_keys: Final[list[str]] = []
|
||||||
unnamed_keys_count = 0
|
unnamed_keys_count = 0
|
||||||
if matched_team_ids:
|
if matched_team_ids:
|
||||||
keys: Final = await VerificationTokenRepository(prisma_client).table.find_many(
|
keys: Final = await VerificationTokenRepository(prisma_client).table.find_many(
|
||||||
|
|
@ -177,10 +185,12 @@ async def _find_affected_by_team_patterns(
|
||||||
return new_teams, new_keys, unnamed_keys_count
|
return new_teams, new_keys, unnamed_keys_count
|
||||||
|
|
||||||
|
|
||||||
async def _find_affected_keys_by_alias(prisma_client: object, key_patterns: list, existing_keys: list) -> list:
|
async def _find_affected_keys_by_alias(
|
||||||
|
prisma_client: object, key_patterns: Sequence[str], existing_keys: Sequence[str]
|
||||||
|
) -> list[str]:
|
||||||
"""Find keys whose alias matches the given patterns."""
|
"""Find keys whose alias matches the given patterns."""
|
||||||
|
|
||||||
affected: Final[list] = []
|
affected: Final[list[str]] = []
|
||||||
|
|
||||||
keys: Final = await VerificationTokenRepository(prisma_client).table.find_many(
|
keys: Final = await VerificationTokenRepository(prisma_client).table.find_many(
|
||||||
where=_build_alias_where("key_alias", key_patterns),
|
where=_build_alias_where("key_alias", key_patterns),
|
||||||
|
|
@ -349,8 +359,8 @@ async def estimate_attachment_impact(
|
||||||
sample_teams=["(global scope — affects all teams)"],
|
sample_teams=["(global scope — affects all teams)"],
|
||||||
)
|
)
|
||||||
|
|
||||||
affected_keys: list = []
|
affected_keys: list[str] = []
|
||||||
affected_teams: list = []
|
affected_teams: list[str] = []
|
||||||
unnamed_keys = 0
|
unnamed_keys = 0
|
||||||
unnamed_teams = 0
|
unnamed_teams = 0
|
||||||
|
|
||||||
|
|
@ -358,7 +368,7 @@ async def estimate_attachment_impact(
|
||||||
team_patterns: Final = request.teams or []
|
team_patterns: Final = request.teams or []
|
||||||
|
|
||||||
# Fetch teams once — reused by both tag-based and alias-based lookups
|
# Fetch teams once — reused by both tag-based and alias-based lookups
|
||||||
all_teams: list = []
|
all_teams: Sequence[prisma_models.LiteLLM_TeamTable] = []
|
||||||
if tag_patterns or team_patterns:
|
if tag_patterns or team_patterns:
|
||||||
all_teams = await _fetch_all_teams(prisma_client)
|
all_teams = await _fetch_all_teams(prisma_client)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ class _PromptTableActions(Protocol):
|
||||||
|
|
||||||
def create(self, *, data: Mapping[str, str | int | None]) -> Awaitable[_PromptRow]: ...
|
def create(self, *, data: Mapping[str, str | int | None]) -> Awaitable[_PromptRow]: ...
|
||||||
|
|
||||||
def update(self, *, where: Mapping[str, str | int], data: Mapping[str, str]) -> Awaitable[_PromptRow]: ...
|
def update(self, *, where: Mapping[str, str | int], data: Mapping[str, str]) -> Awaitable[_PromptRow | None]: ...
|
||||||
|
|
||||||
def delete_many(self, *, where: Mapping[str, str]) -> Awaitable[int]: ...
|
def delete_many(self, *, where: Mapping[str, str]) -> Awaitable[int]: ...
|
||||||
|
|
||||||
|
|
@ -1157,6 +1157,12 @@ async def patch_prompt(
|
||||||
data=update_data,
|
data=update_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if updated_prompt_db_entry is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail=f"Prompt with ID {base_prompt_id} not found in environment {env}",
|
||||||
|
)
|
||||||
|
|
||||||
updated_prompt_spec: Final = create_versioned_prompt_spec(db_prompt=updated_prompt_db_entry)
|
updated_prompt_spec: Final = create_versioned_prompt_spec(db_prompt=updated_prompt_db_entry)
|
||||||
|
|
||||||
return _reload_prompt_in_registry(IN_MEMORY_PROMPT_REGISTRY, versioned_id, updated_prompt_spec)
|
return _reload_prompt_in_registry(IN_MEMORY_PROMPT_REGISTRY, versioned_id, updated_prompt_spec)
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,7 @@ if TYPE_CHECKING:
|
||||||
from aiohttp import ClientSession
|
from aiohttp import ClientSession
|
||||||
from fastapi.routing import APIRoute
|
from fastapi.routing import APIRoute
|
||||||
from opentelemetry.trace import Span as _Span
|
from opentelemetry.trace import Span as _Span
|
||||||
|
from prisma import models as prisma_models
|
||||||
|
|
||||||
from litellm.integrations.opentelemetry import OpenTelemetry
|
from litellm.integrations.opentelemetry import OpenTelemetry
|
||||||
|
|
||||||
|
|
@ -634,6 +635,7 @@ from litellm.proxy.utils import (
|
||||||
from litellm.proxy.video_endpoints.endpoints import router as video_router
|
from litellm.proxy.video_endpoints.endpoints import router as video_router
|
||||||
from litellm.repositories.base_repository import SupportsModelDump
|
from litellm.repositories.base_repository import SupportsModelDump
|
||||||
from litellm.repositories.credentials_repository import CredentialsRepository
|
from litellm.repositories.credentials_repository import CredentialsRepository
|
||||||
|
from litellm.repositories.prisma_protocols import TableActions
|
||||||
from litellm.router import (
|
from litellm.router import (
|
||||||
AssistantsTypedDict,
|
AssistantsTypedDict,
|
||||||
Deployment,
|
Deployment,
|
||||||
|
|
@ -1642,12 +1644,21 @@ class _InvitationLinkRow(Protocol):
|
||||||
class _UserTableRow(Protocol):
|
class _UserTableRow(Protocol):
|
||||||
user_id: str
|
user_id: str
|
||||||
user_email: str | None
|
user_email: str | None
|
||||||
user_role: str
|
user_role: str | None
|
||||||
|
|
||||||
|
|
||||||
class _ModelTableRow(Protocol):
|
class _UserTeamsRow(Protocol):
|
||||||
model_id: str | None
|
@property
|
||||||
created_by: str | None
|
def teams(self) -> Sequence[str]: ...
|
||||||
|
|
||||||
|
|
||||||
|
_ProxyModelRow: TypeAlias = "prisma_models.LiteLLM_ProxyModelTable"
|
||||||
|
|
||||||
|
|
||||||
|
def _config_param_table(client: PrismaClient | None) -> TableActions[_ConfigParamRow]:
|
||||||
|
return cast( # cast-ok: this is prisma's LiteLLM_Config actions object, which parses its Json column to a mapping
|
||||||
|
"TableActions[_ConfigParamRow]", ConfigRepository(client).table
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class _TTFTRow(TypedDict):
|
class _TTFTRow(TypedDict):
|
||||||
|
|
@ -4376,7 +4387,7 @@ class ProxyConfig:
|
||||||
if prisma_client is None or not (general_settings.get("store_model_in_db", False) is True or store_model_in_db):
|
if prisma_client is None or not (general_settings.get("store_model_in_db", False) is True or store_model_in_db):
|
||||||
return
|
return
|
||||||
|
|
||||||
row: Final[_ConfigParamRow | None] = await ConfigRepository(prisma_client).table.find_first(
|
row: Final[_ConfigParamRow | None] = await _config_param_table(prisma_client).find_first(
|
||||||
where={"param_name": "environment_variables"}
|
where={"param_name": "environment_variables"}
|
||||||
)
|
)
|
||||||
existing: Final[dict] = dict(row.param_value) if row is not None and row.param_value is not None else {}
|
existing: Final[dict] = dict(row.param_value) if row is not None and row.param_value is not None else {}
|
||||||
|
|
@ -6232,7 +6243,7 @@ class ProxyConfig:
|
||||||
4. Update router settings
|
4. Update router settings
|
||||||
"""
|
"""
|
||||||
if llm_router is not None and prisma_client is not None:
|
if llm_router is not None and prisma_client is not None:
|
||||||
db_router_settings: Final[_ConfigParamRow | None] = await ConfigRepository(prisma_client).table.find_first(
|
db_router_settings: Final[_ConfigParamRow | None] = await _config_param_table(prisma_client).find_first(
|
||||||
where={"param_name": "router_settings"}
|
where={"param_name": "router_settings"}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -6660,7 +6671,7 @@ class ProxyConfig:
|
||||||
def _should_load_db_object(self, object_type: str | SupportedDBObjectType) -> bool:
|
def _should_load_db_object(self, object_type: str | SupportedDBObjectType) -> bool:
|
||||||
return should_load_db_object(object_type=object_type)
|
return should_load_db_object(object_type=object_type)
|
||||||
|
|
||||||
async def _get_models_from_db(self, prisma_client: PrismaClient) -> list | None:
|
async def _get_models_from_db(self, prisma_client: PrismaClient) -> Sequence[_ProxyModelRow] | None:
|
||||||
"""
|
"""
|
||||||
Fetch all model deployments from the DB.
|
Fetch all model deployments from the DB.
|
||||||
|
|
||||||
|
|
@ -6670,7 +6681,7 @@ class ProxyConfig:
|
||||||
as "all models deleted" and must not evict existing router deployments.
|
as "all models deleted" and must not evict existing router deployments.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
new_models: Final[list[_ModelTableRow]] = await ModelRepository(prisma_client).table.find_many()
|
new_models: Final[Sequence[_ProxyModelRow]] = await ModelRepository(prisma_client).table.find_many()
|
||||||
return new_models
|
return new_models
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
verbose_proxy_logger.exception(
|
verbose_proxy_logger.exception(
|
||||||
|
|
@ -6957,10 +6968,13 @@ class ProxyConfig:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sso_settings: Final[_SSOConfigRow | None] = await call_with_db_reconnect_retry(
|
sso_settings: Final[_SSOConfigRow | None] = cast( # cast-ok: prisma Json stub is `str`, runtime is a dict
|
||||||
prisma_client,
|
"_SSOConfigRow | None",
|
||||||
lambda: SSOConfigRepository(prisma_client).table.find_unique(where={"id": "sso_config"}),
|
await call_with_db_reconnect_retry(
|
||||||
reason="init_sso_settings_in_db_lookup_failure",
|
prisma_client,
|
||||||
|
lambda: SSOConfigRepository(prisma_client).table.find_unique(where={"id": "sso_config"}),
|
||||||
|
reason="init_sso_settings_in_db_lookup_failure",
|
||||||
|
),
|
||||||
)
|
)
|
||||||
if sso_settings is not None:
|
if sso_settings is not None:
|
||||||
sso_settings.sso_settings.pop("role_mappings", None)
|
sso_settings.sso_settings.pop("role_mappings", None)
|
||||||
|
|
@ -6988,12 +7002,15 @@ class ProxyConfig:
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
db_record: Final[_ConfigOverridesRow | None] = await call_with_db_reconnect_retry(
|
db_record: Final[_ConfigOverridesRow | None] = cast( # cast-ok: prisma Json stub is `str`, runtime dict
|
||||||
prisma_client,
|
"_ConfigOverridesRow | None",
|
||||||
lambda: ConfigOverridesRepository(prisma_client).table.find_unique(
|
await call_with_db_reconnect_retry(
|
||||||
where={"config_type": "hashicorp_vault"}
|
prisma_client,
|
||||||
|
lambda: ConfigOverridesRepository(prisma_client).table.find_unique(
|
||||||
|
where={"config_type": "hashicorp_vault"}
|
||||||
|
),
|
||||||
|
reason="init_hashicorp_vault_config_override_lookup_failure",
|
||||||
),
|
),
|
||||||
reason="init_hashicorp_vault_config_override_lookup_failure",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if db_record is None or db_record.config_value is None:
|
if db_record is None or db_record.config_value is None:
|
||||||
|
|
@ -8841,8 +8858,9 @@ class ProxyStartupEvent:
|
||||||
|
|
||||||
if prisma_client is None:
|
if prisma_client is None:
|
||||||
return
|
return
|
||||||
db_record: Final[_UISettingsRow | None] = await UISettingsRepository(prisma_client).table.find_unique(
|
db_record: Final[_UISettingsRow | None] = cast( # cast-ok: prisma Json stub is `str`, runtime is a dict
|
||||||
where={"id": "ui_settings"}
|
"_UISettingsRow | None",
|
||||||
|
await UISettingsRepository(prisma_client).table.find_unique(where={"id": "ui_settings"}),
|
||||||
)
|
)
|
||||||
if db_record and db_record.ui_settings:
|
if db_record and db_record.ui_settings:
|
||||||
raw: Final = db_record.ui_settings
|
raw: Final = db_record.ui_settings
|
||||||
|
|
@ -9005,7 +9023,7 @@ class ProxyStartupEvent:
|
||||||
# but YAML config has False.
|
# but YAML config has False.
|
||||||
if store_model_in_db is not True and prisma_client is not None:
|
if store_model_in_db is not True and prisma_client is not None:
|
||||||
try:
|
try:
|
||||||
_db_gs_record: Final[_ConfigParamRow | None] = await ConfigRepository(prisma_client).table.find_first(
|
_db_gs_record: Final[_ConfigParamRow | None] = await _config_param_table(prisma_client).find_first(
|
||||||
where={"param_name": "general_settings"}
|
where={"param_name": "general_settings"}
|
||||||
)
|
)
|
||||||
if _db_gs_record is not None and isinstance(_db_gs_record.param_value, dict):
|
if _db_gs_record is not None and isinstance(_db_gs_record.param_value, dict):
|
||||||
|
|
@ -12150,14 +12168,14 @@ async def _check_if_model_is_user_added(
|
||||||
id = model.get("model_info", {}).get("id", None)
|
id = model.get("model_info", {}).get("id", None)
|
||||||
if id is None:
|
if id is None:
|
||||||
continue
|
continue
|
||||||
db_model: _ModelTableRow | None = await ModelRepository(prisma_client).table.find_unique(where={"model_id": id})
|
db_model: _ProxyModelRow | None = await ModelRepository(prisma_client).table.find_unique(where={"model_id": id})
|
||||||
if db_model is not None:
|
if db_model is not None:
|
||||||
if db_model.created_by == user_api_key_dict.user_id:
|
if db_model.created_by == user_api_key_dict.user_id:
|
||||||
filtered_models.append(model)
|
filtered_models.append(model)
|
||||||
return filtered_models
|
return filtered_models
|
||||||
|
|
||||||
|
|
||||||
def _check_if_model_is_team_model(models: list[DeploymentTypedDict], user_row: LiteLLM_UserTable) -> list[dict]:
|
def _check_if_model_is_team_model(models: list[DeploymentTypedDict], user_row: _UserTeamsRow) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
Check if model is a team model
|
Check if model is a team model
|
||||||
|
|
||||||
|
|
@ -12210,10 +12228,11 @@ async def non_admin_all_models(
|
||||||
raise HTTPException(status_code=400, detail={"error": "User not found"})
|
raise HTTPException(status_code=400, detail={"error": "User not found"})
|
||||||
|
|
||||||
# Get all models that are team models, when model team_id == user_row.teams
|
# Get all models that are team models, when model team_id == user_row.teams
|
||||||
all_models += _check_if_model_is_team_model(
|
if user_row is not None:
|
||||||
models=llm_router.get_model_list() or [],
|
all_models += _check_if_model_is_team_model(
|
||||||
user_row=user_row,
|
models=llm_router.get_model_list() or [],
|
||||||
)
|
user_row=user_row,
|
||||||
|
)
|
||||||
|
|
||||||
# de-duplicate models. Only return unique model ids
|
# de-duplicate models. Only return unique model ids
|
||||||
unique_models: Final = _deduplicate_litellm_router_models(models=all_models)
|
unique_models: Final = _deduplicate_litellm_router_models(models=all_models)
|
||||||
|
|
@ -12637,7 +12656,7 @@ async def _fetch_db_models_for_search(
|
||||||
|
|
||||||
db_models_total_count: Final = await ModelRepository(prisma_client).table.count(where=db_where_condition)
|
db_models_total_count: Final = await ModelRepository(prisma_client).table.count(where=db_where_condition)
|
||||||
|
|
||||||
db_models_raw: list = []
|
db_models_raw: Sequence[_ProxyModelRow] = []
|
||||||
if take_limit > 0:
|
if take_limit > 0:
|
||||||
db_models_raw = await ModelRepository(prisma_client).table.find_many(
|
db_models_raw = await ModelRepository(prisma_client).table.find_many(
|
||||||
where=db_where_condition,
|
where=db_where_condition,
|
||||||
|
|
@ -13027,7 +13046,7 @@ async def _gather_team_accessible_model_ids(
|
||||||
try:
|
try:
|
||||||
if team_object.models and SpecialModelNames.all_proxy_models.value not in team_object.models:
|
if team_object.models and SpecialModelNames.all_proxy_models.value not in team_object.models:
|
||||||
_resolved_names: Final = _team_models_resolve_to_names(team_object.models, access_groups)
|
_resolved_names: Final = _team_models_resolve_to_names(team_object.models, access_groups)
|
||||||
db_models: Final[Sequence[_ModelTableRow]] = await ModelRepository(prisma_client).table.find_many(
|
db_models: Final[Sequence[_ProxyModelRow]] = await ModelRepository(prisma_client).table.find_many(
|
||||||
where={"model_name": {"in": _resolved_names}}
|
where={"model_name": {"in": _resolved_names}}
|
||||||
)
|
)
|
||||||
for db_model in db_models:
|
for db_model in db_models:
|
||||||
|
|
@ -14501,14 +14520,18 @@ async def alerting_settings(
|
||||||
)
|
)
|
||||||
|
|
||||||
## get general settings from db
|
## get general settings from db
|
||||||
db_general_settings: Final = await ConfigRepository(prisma_client).table.find_first(
|
db_general_settings: Final = await _config_param_table(prisma_client).find_first(
|
||||||
where={"param_name": "general_settings"}
|
where={"param_name": "general_settings"}
|
||||||
)
|
)
|
||||||
|
|
||||||
if db_general_settings is not None and db_general_settings.param_value is not None:
|
if db_general_settings is not None and db_general_settings.param_value is not None:
|
||||||
db_general_settings_dict: Final = dict(db_general_settings.param_value)
|
db_general_settings_dict: Final = dict(db_general_settings.param_value)
|
||||||
alerting_args_dict: dict = db_general_settings_dict.get("alerting_args", {})
|
alerting_args_dict: dict = cast( # cast-ok: ConfigGeneralSettings validates alerting_args as a dict on write
|
||||||
alerting_values: list | None = db_general_settings_dict.get("alerting")
|
dict[str, JsonValue], db_general_settings_dict.get("alerting_args", {})
|
||||||
|
)
|
||||||
|
alerting_values: list | None = cast( # cast-ok: ConfigGeneralSettings validates alerting as a list on write
|
||||||
|
list[JsonValue] | None, db_general_settings_dict.get("alerting")
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
alerting_args_dict = {}
|
alerting_args_dict = {}
|
||||||
alerting_values = None
|
alerting_values = None
|
||||||
|
|
@ -15059,7 +15082,7 @@ async def onboarding(invite_link: str, request: Request):
|
||||||
user_id=user_obj.user_id,
|
user_id=user_obj.user_id,
|
||||||
key=onboarding_token,
|
key=onboarding_token,
|
||||||
user_email=user_obj.user_email,
|
user_email=user_obj.user_email,
|
||||||
user_role=user_obj.user_role,
|
user_role=user_obj.user_role, # pyright: ignore[reportArgumentType] # nullable DB column, no unset contract
|
||||||
login_method="username_password",
|
login_method="username_password",
|
||||||
premium_user=premium_user,
|
premium_user=premium_user,
|
||||||
auth_header_name=general_settings.get("litellm_key_header_name", "Authorization"),
|
auth_header_name=general_settings.get("litellm_key_header_name", "Authorization"),
|
||||||
|
|
@ -15168,7 +15191,7 @@ async def _generate_onboarding_ui_session_token(user_obj: _UserTableRow) -> str:
|
||||||
user_id=user_obj.user_id,
|
user_id=user_obj.user_id,
|
||||||
key=key,
|
key=key,
|
||||||
user_email=user_obj.user_email,
|
user_email=user_obj.user_email,
|
||||||
user_role=user_obj.user_role,
|
user_role=user_obj.user_role, # pyright: ignore[reportArgumentType] # nullable DB column, no unset contract
|
||||||
login_method="username_password",
|
login_method="username_password",
|
||||||
premium_user=premium_user,
|
premium_user=premium_user,
|
||||||
auth_header_name=general_settings.get("litellm_key_header_name", "Authorization"),
|
auth_header_name=general_settings.get("litellm_key_header_name", "Authorization"),
|
||||||
|
|
@ -15729,7 +15752,7 @@ async def update_config(
|
||||||
raise Exception("No DB Connected")
|
raise Exception("No DB Connected")
|
||||||
|
|
||||||
async def _read_section(param_name: str) -> dict:
|
async def _read_section(param_name: str) -> dict:
|
||||||
row: Final[_ConfigParamRow | None] = await ConfigRepository(prisma_client).table.find_first(
|
row: Final[_ConfigParamRow | None] = await _config_param_table(prisma_client).find_first(
|
||||||
where={"param_name": param_name}
|
where={"param_name": param_name}
|
||||||
)
|
)
|
||||||
if row is None or row.param_value is None:
|
if row is None or row.param_value is None:
|
||||||
|
|
@ -15986,7 +16009,7 @@ async def update_config_general_settings(
|
||||||
)
|
)
|
||||||
|
|
||||||
## get general settings from db
|
## get general settings from db
|
||||||
db_general_settings: Final = await ConfigRepository(prisma_client).table.find_first(
|
db_general_settings: Final = await _config_param_table(prisma_client).find_first(
|
||||||
where={"param_name": "general_settings"}
|
where={"param_name": "general_settings"}
|
||||||
)
|
)
|
||||||
### update value
|
### update value
|
||||||
|
|
@ -16004,7 +16027,7 @@ async def update_config_general_settings(
|
||||||
if data.field_name == "plugins":
|
if data.field_name == "plugins":
|
||||||
field_value = _preserve_redacted_plugin_keys(field_value, general_settings.get("plugins"))
|
field_value = _preserve_redacted_plugin_keys(field_value, general_settings.get("plugins"))
|
||||||
|
|
||||||
general_settings[data.field_name] = field_value
|
general_settings[data.field_name] = cast(JsonValue, field_value) # cast-ok: ConfigGeneralSettings validated it
|
||||||
|
|
||||||
response: Final = await ConfigRepository(prisma_client).table.upsert(
|
response: Final = await ConfigRepository(prisma_client).table.upsert(
|
||||||
where={"param_name": "general_settings"},
|
where={"param_name": "general_settings"},
|
||||||
|
|
@ -16024,7 +16047,7 @@ async def update_config_general_settings(
|
||||||
)
|
)
|
||||||
|
|
||||||
if data.field_name == "plugins":
|
if data.field_name == "plugins":
|
||||||
register_plugins_from_config(general_settings)
|
register_plugins_from_config(cast(dict[str, object], general_settings)) # cast-ok: the callee only reads it
|
||||||
_apply_ssrf_general_settings(general_settings)
|
_apply_ssrf_general_settings(general_settings)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
@ -16200,7 +16223,7 @@ async def get_config_general_settings(
|
||||||
)
|
)
|
||||||
|
|
||||||
## get general settings from db
|
## get general settings from db
|
||||||
db_general_settings: Final[_ConfigParamRow | None] = await ConfigRepository(prisma_client).table.find_first(
|
db_general_settings: Final[_ConfigParamRow | None] = await _config_param_table(prisma_client).find_first(
|
||||||
where={"param_name": "general_settings"}
|
where={"param_name": "general_settings"}
|
||||||
)
|
)
|
||||||
### pop the value
|
### pop the value
|
||||||
|
|
@ -16389,7 +16412,7 @@ async def get_config_list(
|
||||||
is_full_admin: Final = user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN
|
is_full_admin: Final = user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN
|
||||||
|
|
||||||
## get general settings from db
|
## get general settings from db
|
||||||
db_general_settings: Final[_ConfigParamRow | None] = await ConfigRepository(prisma_client).table.find_first(
|
db_general_settings: Final[_ConfigParamRow | None] = await _config_param_table(prisma_client).find_first(
|
||||||
where={"param_name": "general_settings"}
|
where={"param_name": "general_settings"}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -16485,7 +16508,7 @@ async def get_config_list(
|
||||||
)
|
)
|
||||||
return_val.append(_response_obj)
|
return_val.append(_response_obj)
|
||||||
|
|
||||||
db_litellm_settings_row: Final[_ConfigParamRow | None] = await ConfigRepository(prisma_client).table.find_first(
|
db_litellm_settings_row: Final[_ConfigParamRow | None] = await _config_param_table(prisma_client).find_first(
|
||||||
where={"param_name": "litellm_settings"}
|
where={"param_name": "litellm_settings"}
|
||||||
)
|
)
|
||||||
db_litellm_settings: Final[dict] = (
|
db_litellm_settings: Final[dict] = (
|
||||||
|
|
@ -16562,7 +16585,7 @@ async def delete_config_general_settings(
|
||||||
)
|
)
|
||||||
|
|
||||||
## get general settings from db
|
## get general settings from db
|
||||||
db_general_settings: Final[_ConfigParamRow | None] = await ConfigRepository(prisma_client).table.find_first(
|
db_general_settings: Final[_ConfigParamRow | None] = await _config_param_table(prisma_client).find_first(
|
||||||
where={"param_name": "general_settings"}
|
where={"param_name": "general_settings"}
|
||||||
)
|
)
|
||||||
### pop the value
|
### pop the value
|
||||||
|
|
@ -17129,7 +17152,7 @@ async def reload_anthropic_beta_headers(
|
||||||
last_anthropic_beta_headers_reload = current_time.isoformat()
|
last_anthropic_beta_headers_reload = current_time.isoformat()
|
||||||
|
|
||||||
# Set force reload flag in database for other pods, preserving existing interval_hours
|
# Set force reload flag in database for other pods, preserving existing interval_hours
|
||||||
existing_beta_config: Final[_ConfigParamRow | None] = await ConfigRepository(prisma_client).table.find_unique(
|
existing_beta_config: Final[_ConfigParamRow | None] = await _config_param_table(prisma_client).find_unique(
|
||||||
where={"param_name": "anthropic_beta_headers_reload_config"}
|
where={"param_name": "anthropic_beta_headers_reload_config"}
|
||||||
)
|
)
|
||||||
existing_beta_interval = None
|
existing_beta_interval = None
|
||||||
|
|
@ -17307,7 +17330,7 @@ async def get_anthropic_beta_headers_reload_status(
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get reload configuration from database
|
# Get reload configuration from database
|
||||||
config_record: Final = await ConfigRepository(prisma_client).table.find_unique(
|
config_record: Final = await _config_param_table(prisma_client).find_unique(
|
||||||
where={"param_name": "anthropic_beta_headers_reload_config"}
|
where={"param_name": "anthropic_beta_headers_reload_config"}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -17321,7 +17344,9 @@ async def get_anthropic_beta_headers_reload_status(
|
||||||
}
|
}
|
||||||
|
|
||||||
config: Final = config_record.param_value
|
config: Final = config_record.param_value
|
||||||
interval_hours: Final = config.get("interval_hours")
|
interval_hours: Final = cast( # cast-ok: every writer of this key stores `hours: int` or an explicit None
|
||||||
|
int | None, config.get("interval_hours")
|
||||||
|
)
|
||||||
|
|
||||||
if interval_hours is None:
|
if interval_hours is None:
|
||||||
verbose_proxy_logger.info("No interval configured, returning not scheduled")
|
verbose_proxy_logger.info("No interval configured, returning not scheduled")
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
import json
|
import json
|
||||||
from typing import Final
|
from collections.abc import Mapping
|
||||||
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Final,
|
||||||
|
Protocol,
|
||||||
|
cast, # noqa: TID251 # the config repository's table protocol omits find_first
|
||||||
|
)
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
|
||||||
|
|
@ -13,6 +19,7 @@ from litellm.proxy.common_utils.encrypt_decrypt_utils import (
|
||||||
)
|
)
|
||||||
from litellm.proxy.management_endpoints.common_utils import _user_has_admin_view
|
from litellm.proxy.management_endpoints.common_utils import _user_has_admin_view
|
||||||
from litellm.repositories.config_repository import ConfigRepository
|
from litellm.repositories.config_repository import ConfigRepository
|
||||||
|
from litellm.repositories.prisma_protocols import TableActions
|
||||||
from litellm.types.proxy.cloudzero_endpoints import (
|
from litellm.types.proxy.cloudzero_endpoints import (
|
||||||
CloudZeroExportRequest,
|
CloudZeroExportRequest,
|
||||||
CloudZeroExportResponse,
|
CloudZeroExportResponse,
|
||||||
|
|
@ -22,6 +29,9 @@ from litellm.types.proxy.cloudzero_endpoints import (
|
||||||
CloudZeroSettingsView,
|
CloudZeroSettingsView,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from litellm.proxy.proxy_server import PrismaClient
|
||||||
|
|
||||||
router: Final = APIRouter()
|
router: Final = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -29,6 +39,18 @@ router: Final = APIRouter()
|
||||||
_sensitive_masker: Final = SensitiveDataMasker()
|
_sensitive_masker: Final = SensitiveDataMasker()
|
||||||
|
|
||||||
|
|
||||||
|
class _CloudZeroConfigRow(Protocol):
|
||||||
|
"""The ``LiteLLM_Config`` row holding ``cloudzero_settings``, as this module reads it."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def param_value(self) -> str | Mapping[str, str] | None: ...
|
||||||
|
|
||||||
|
|
||||||
|
def _config_table(prisma_client: "PrismaClient") -> TableActions[_CloudZeroConfigRow]:
|
||||||
|
repository_table: Final = ConfigRepository(prisma_client).table
|
||||||
|
return cast(TableActions[_CloudZeroConfigRow], repository_table) # cast-ok: repo protocol omits find_first
|
||||||
|
|
||||||
|
|
||||||
async def _set_cloudzero_settings(api_key: str, connection_id: str, timezone: str):
|
async def _set_cloudzero_settings(api_key: str, connection_id: str, timezone: str):
|
||||||
"""
|
"""
|
||||||
Store CloudZero settings in the database with encrypted API key.
|
Store CloudZero settings in the database with encrypted API key.
|
||||||
|
|
@ -82,9 +104,7 @@ async def _get_cloudzero_settings():
|
||||||
detail={"error": CommonProxyErrors.db_not_connected_error.value},
|
detail={"error": CommonProxyErrors.db_not_connected_error.value},
|
||||||
)
|
)
|
||||||
|
|
||||||
cloudzero_config: Final = await ConfigRepository(prisma_client).table.find_first(
|
cloudzero_config: Final = await _config_table(prisma_client).find_first(where={"param_name": "cloudzero_settings"})
|
||||||
where={"param_name": "cloudzero_settings"}
|
|
||||||
)
|
|
||||||
if cloudzero_config is None or cloudzero_config.param_value is None:
|
if cloudzero_config is None or cloudzero_config.param_value is None:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
@ -268,7 +288,7 @@ async def is_cloudzero_setup_in_db() -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Check for CloudZero settings in database
|
# Check for CloudZero settings in database
|
||||||
cloudzero_config: Final = await ConfigRepository(prisma_client).table.find_first(
|
cloudzero_config: Final = await _config_table(prisma_client).find_first(
|
||||||
where={"param_name": "cloudzero_settings"}
|
where={"param_name": "cloudzero_settings"}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -530,7 +550,7 @@ async def delete_cloudzero_settings(
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if CloudZero settings exist
|
# Check if CloudZero settings exist
|
||||||
cloudzero_config: Final = await ConfigRepository(prisma_client).table.find_first(
|
cloudzero_config: Final = await _config_table(prisma_client).find_first(
|
||||||
where={"param_name": "cloudzero_settings"}
|
where={"param_name": "cloudzero_settings"}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,22 @@ import json
|
||||||
import os
|
import os
|
||||||
from collections.abc import Mapping, Sequence
|
from collections.abc import Mapping, Sequence
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import TYPE_CHECKING, Annotated, Any, Final, Literal, NamedTuple, Protocol, TypedDict, TypeVar
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Annotated,
|
||||||
|
Any,
|
||||||
|
Final,
|
||||||
|
Literal,
|
||||||
|
NamedTuple,
|
||||||
|
Protocol,
|
||||||
|
TypedDict,
|
||||||
|
TypeVar,
|
||||||
|
cast, # noqa: TID251 # prisma group_by returns untyped aggregate mappings
|
||||||
|
)
|
||||||
|
|
||||||
import fastapi
|
import fastapi
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||||
|
from typing_extensions import ReadOnly
|
||||||
|
|
||||||
import litellm
|
import litellm
|
||||||
from litellm._logging import verbose_proxy_logger
|
from litellm._logging import verbose_proxy_logger
|
||||||
|
|
@ -23,6 +35,7 @@ from litellm.proxy.spend_tracking.spend_tracking_utils import (
|
||||||
get_spend_by_team_and_customer,
|
get_spend_by_team_and_customer,
|
||||||
)
|
)
|
||||||
from litellm.proxy.utils import handle_exception_on_proxy
|
from litellm.proxy.utils import handle_exception_on_proxy
|
||||||
|
from litellm.repositories.prisma_protocols import TableActions
|
||||||
from litellm.repositories.table_repositories import SpendLogsRepository
|
from litellm.repositories.table_repositories import SpendLogsRepository
|
||||||
from litellm.repositories.team_repository import TeamRepository
|
from litellm.repositories.team_repository import TeamRepository
|
||||||
from litellm.repositories.verification_token_repository import (
|
from litellm.repositories.verification_token_repository import (
|
||||||
|
|
@ -30,6 +43,8 @@ from litellm.repositories.verification_token_repository import (
|
||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from prisma import models as prisma_models
|
||||||
|
|
||||||
from litellm.proxy.proxy_server import PrismaClient
|
from litellm.proxy.proxy_server import PrismaClient
|
||||||
from litellm.proxy.spend_tracking.cold_storage_handler import ColdStorageHandler
|
from litellm.proxy.spend_tracking.cold_storage_handler import ColdStorageHandler
|
||||||
else:
|
else:
|
||||||
|
|
@ -139,6 +154,18 @@ class _SessionSpendRow(TypedDict):
|
||||||
mcp_tool_call_spend: float
|
mcp_tool_call_spend: float
|
||||||
|
|
||||||
|
|
||||||
|
class _SpendSumAggregate(TypedDict, total=False):
|
||||||
|
spend: ReadOnly[float]
|
||||||
|
|
||||||
|
|
||||||
|
class _SpendGroupByRow(TypedDict):
|
||||||
|
api_key: ReadOnly[str]
|
||||||
|
user: ReadOnly[str | None]
|
||||||
|
model: ReadOnly[str]
|
||||||
|
startTime: ReadOnly[object]
|
||||||
|
_sum: ReadOnly[_SpendSumAggregate]
|
||||||
|
|
||||||
|
|
||||||
async def _query_raw(prisma_client: PrismaClient, sql_query: str, *args: object) -> Sequence[_RowT]:
|
async def _query_raw(prisma_client: PrismaClient, sql_query: str, *args: object) -> Sequence[_RowT]:
|
||||||
"""Run a raw read query and return its rows as the row type the caller declares."""
|
"""Run a raw read query and return its rows as the row type the caller declares."""
|
||||||
return await prisma_client.db.query_raw(sql_query, *args)
|
return await prisma_client.db.query_raw(sql_query, *args)
|
||||||
|
|
@ -149,24 +176,6 @@ async def _query_raw_or_none(prisma_client: PrismaClient, sql_query: str, *args:
|
||||||
return await _query_raw(prisma_client, sql_query, *args)
|
return await _query_raw(prisma_client, sql_query, *args)
|
||||||
|
|
||||||
|
|
||||||
class _SpendLogsTable(Protocol):
|
|
||||||
"""The subset of the Prisma spend-logs table API this module uses."""
|
|
||||||
|
|
||||||
async def find_many(
|
|
||||||
self, *, where: Mapping[str, object], order: Mapping[str, str]
|
|
||||||
) -> Sequence[_SupportsModelDump]: ...
|
|
||||||
|
|
||||||
async def find_unique(
|
|
||||||
self, *, where: Mapping[str, object], include: None = None
|
|
||||||
) -> _SpendLogOwnershipRow | None: ...
|
|
||||||
|
|
||||||
async def count(self, *, where: Mapping[str, object]) -> int: ...
|
|
||||||
|
|
||||||
async def group_by(
|
|
||||||
self, *, by: Sequence[str], where: Mapping[str, object], count: Mapping[str, bool]
|
|
||||||
) -> Sequence[_SessionCountRow]: ...
|
|
||||||
|
|
||||||
|
|
||||||
class _TeamTable(Protocol):
|
class _TeamTable(Protocol):
|
||||||
"""The subset of the Prisma team table API this module uses."""
|
"""The subset of the Prisma team table API this module uses."""
|
||||||
|
|
||||||
|
|
@ -183,7 +192,7 @@ class _VerificationTokenTable(Protocol):
|
||||||
async def update_many(self, *, data: Mapping[str, float], where: Mapping[str, object]) -> int: ...
|
async def update_many(self, *, data: Mapping[str, float], where: Mapping[str, object]) -> int: ...
|
||||||
|
|
||||||
|
|
||||||
def _spend_logs_table(prisma_client: PrismaClient) -> _SpendLogsTable:
|
def _spend_logs_table(prisma_client: PrismaClient) -> TableActions["prisma_models.LiteLLM_SpendLogs"]:
|
||||||
return SpendLogsRepository(prisma_client).table
|
return SpendLogsRepository(prisma_client).table
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -221,11 +230,12 @@ async def _count_logs_per_session(
|
||||||
prisma_client: PrismaClient, session_ids: Sequence[str | None]
|
prisma_client: PrismaClient, session_ids: Sequence[str | None]
|
||||||
) -> Sequence[_SessionCountRow]:
|
) -> Sequence[_SessionCountRow]:
|
||||||
"""Count spend log rows per session for the given session ids."""
|
"""Count spend log rows per session for the given session ids."""
|
||||||
return await _spend_logs_table(prisma_client).group_by(
|
rows: Final = await _spend_logs_table(prisma_client).group_by(
|
||||||
by=["session_id"],
|
by=["session_id"],
|
||||||
where={"session_id": {"in": session_ids}},
|
where={"session_id": {"in": session_ids}},
|
||||||
count={"session_id": True},
|
count={"session_id": True},
|
||||||
)
|
)
|
||||||
|
return cast(Sequence[_SessionCountRow], rows) # cast-ok: group_by(count=) shape is fixed by the by/count args
|
||||||
|
|
||||||
|
|
||||||
async def _find_team_row(prisma_client: PrismaClient, team_id: str) -> _SupportsModelDump | None:
|
async def _find_team_row(prisma_client: PrismaClient, team_id: str) -> _SupportsModelDump | None:
|
||||||
|
|
@ -2974,8 +2984,9 @@ async def view_spend_logs(
|
||||||
)
|
)
|
||||||
|
|
||||||
if isinstance(response, list) and len(response) > 0 and isinstance(response[0], dict):
|
if isinstance(response, list) and len(response) > 0 and isinstance(response[0], dict):
|
||||||
|
spend_rows: Final = cast(Sequence[_SpendGroupByRow], response) # cast-ok: by/sum fix the shape
|
||||||
result: Final[dict] = {}
|
result: Final[dict] = {}
|
||||||
for record in response:
|
for record in spend_rows:
|
||||||
dt_object = datetime.strptime(str(record["startTime"]), "%Y-%m-%dT%H:%M:%S.%fZ")
|
dt_object = datetime.strptime(str(record["startTime"]), "%Y-%m-%dT%H:%M:%S.%fZ")
|
||||||
date = dt_object.date()
|
date = dt_object.date()
|
||||||
if date not in result:
|
if date not in result:
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
import json
|
import json
|
||||||
from typing import Final
|
from collections.abc import Mapping
|
||||||
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Final,
|
||||||
|
Protocol,
|
||||||
|
cast, # noqa: TID251 # the config repository's table protocol omits find_first
|
||||||
|
)
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
|
||||||
|
|
@ -14,6 +20,7 @@ from litellm.proxy.common_utils.encrypt_decrypt_utils import (
|
||||||
)
|
)
|
||||||
from litellm.proxy.management_endpoints.common_utils import _user_has_admin_view
|
from litellm.proxy.management_endpoints.common_utils import _user_has_admin_view
|
||||||
from litellm.repositories.config_repository import ConfigRepository
|
from litellm.repositories.config_repository import ConfigRepository
|
||||||
|
from litellm.repositories.prisma_protocols import TableActions
|
||||||
from litellm.types.proxy.vantage_endpoints import (
|
from litellm.types.proxy.vantage_endpoints import (
|
||||||
VantageDryRunRequest,
|
VantageDryRunRequest,
|
||||||
VantageExportRequest,
|
VantageExportRequest,
|
||||||
|
|
@ -24,6 +31,9 @@ from litellm.types.proxy.vantage_endpoints import (
|
||||||
VantageSettingsView,
|
VantageSettingsView,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from litellm.proxy.proxy_server import PrismaClient
|
||||||
|
|
||||||
router: Final = APIRouter()
|
router: Final = APIRouter()
|
||||||
|
|
||||||
_sensitive_masker: Final = SensitiveDataMasker()
|
_sensitive_masker: Final = SensitiveDataMasker()
|
||||||
|
|
@ -31,6 +41,18 @@ _sensitive_masker: Final = SensitiveDataMasker()
|
||||||
VANTAGE_SETTINGS_PARAM_NAME: Final = "vantage_settings"
|
VANTAGE_SETTINGS_PARAM_NAME: Final = "vantage_settings"
|
||||||
|
|
||||||
|
|
||||||
|
class _VantageConfigRow(Protocol):
|
||||||
|
"""The ``LiteLLM_Config`` row holding ``vantage_settings``, as this module reads it."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def param_value(self) -> str | Mapping[str, str] | None: ...
|
||||||
|
|
||||||
|
|
||||||
|
def _config_table(prisma_client: "PrismaClient") -> TableActions[_VantageConfigRow]:
|
||||||
|
repository_table: Final = ConfigRepository(prisma_client).table
|
||||||
|
return cast(TableActions[_VantageConfigRow], repository_table) # cast-ok: repo protocol omits find_first
|
||||||
|
|
||||||
|
|
||||||
def _get_registered_vantage_logger():
|
def _get_registered_vantage_logger():
|
||||||
"""Return the VantageLogger already registered in litellm.callbacks, if any."""
|
"""Return the VantageLogger already registered in litellm.callbacks, if any."""
|
||||||
from litellm.integrations.vantage.vantage_logger import VantageLogger
|
from litellm.integrations.vantage.vantage_logger import VantageLogger
|
||||||
|
|
@ -82,7 +104,7 @@ async def _get_vantage_settings():
|
||||||
detail={"error": CommonProxyErrors.db_not_connected_error.value},
|
detail={"error": CommonProxyErrors.db_not_connected_error.value},
|
||||||
)
|
)
|
||||||
|
|
||||||
vantage_config: Final = await ConfigRepository(prisma_client).table.find_first(
|
vantage_config: Final = await _config_table(prisma_client).find_first(
|
||||||
where={"param_name": VANTAGE_SETTINGS_PARAM_NAME}
|
where={"param_name": VANTAGE_SETTINGS_PARAM_NAME}
|
||||||
)
|
)
|
||||||
if vantage_config is None or vantage_config.param_value is None:
|
if vantage_config is None or vantage_config.param_value is None:
|
||||||
|
|
@ -251,7 +273,7 @@ async def is_vantage_setup_in_db() -> bool:
|
||||||
if prisma_client is None:
|
if prisma_client is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
vantage_config: Final = await ConfigRepository(prisma_client).table.find_first(
|
vantage_config: Final = await _config_table(prisma_client).find_first(
|
||||||
where={"param_name": VANTAGE_SETTINGS_PARAM_NAME}
|
where={"param_name": VANTAGE_SETTINGS_PARAM_NAME}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -525,7 +547,7 @@ async def delete_vantage_settings(
|
||||||
detail={"error": CommonProxyErrors.db_not_connected_error.value},
|
detail={"error": CommonProxyErrors.db_not_connected_error.value},
|
||||||
)
|
)
|
||||||
|
|
||||||
vantage_config: Final = await ConfigRepository(prisma_client).table.find_first(
|
vantage_config: Final = await _config_table(prisma_client).find_first(
|
||||||
where={"param_name": VANTAGE_SETTINGS_PARAM_NAME}
|
where={"param_name": VANTAGE_SETTINGS_PARAM_NAME}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,12 @@ import json
|
||||||
import os
|
import os
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from typing import Any, Final, Protocol, TypeVar
|
from typing import (
|
||||||
|
Any,
|
||||||
|
Final,
|
||||||
|
Protocol,
|
||||||
|
cast, # noqa: TID251 # prisma types Json columns as fields.Json but de-serializes them to plain python on read
|
||||||
|
)
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from fastapi import APIRouter, Body, Depends, File, HTTPException, UploadFile
|
from fastapi import APIRouter, Body, Depends, File, HTTPException, UploadFile
|
||||||
|
|
@ -25,6 +30,7 @@ from litellm.proxy.spend_tracking.ptu_feature_flag import is_ptu_cost_attributio
|
||||||
from litellm.proxy.utils import invalidate_config_param
|
from litellm.proxy.utils import invalidate_config_param
|
||||||
from litellm.repositories.config_repository import ConfigRepository
|
from litellm.repositories.config_repository import ConfigRepository
|
||||||
from litellm.repositories.organization_repository import OrganizationRepository
|
from litellm.repositories.organization_repository import OrganizationRepository
|
||||||
|
from litellm.repositories.prisma_protocols import TableActions
|
||||||
from litellm.repositories.table_repositories import (
|
from litellm.repositories.table_repositories import (
|
||||||
SSOConfigRepository,
|
SSOConfigRepository,
|
||||||
UISettingsRepository,
|
UISettingsRepository,
|
||||||
|
|
@ -37,29 +43,16 @@ from litellm.types.proxy.management_endpoints.ui_sso import (
|
||||||
|
|
||||||
router: Final = APIRouter()
|
router: Final = APIRouter()
|
||||||
|
|
||||||
_DbRecordT: Final = TypeVar("_DbRecordT", covariant=True)
|
|
||||||
|
|
||||||
|
|
||||||
class _PrismaTableActions(Protocol[_DbRecordT]):
|
|
||||||
async def find_unique(self, where: Mapping[str, object]) -> _DbRecordT | None: ...
|
|
||||||
|
|
||||||
async def update(self, where: Mapping[str, object], data: Mapping[str, object]) -> _DbRecordT: ...
|
|
||||||
|
|
||||||
async def upsert(self, where: Mapping[str, object], data: Mapping[str, object]) -> _DbRecordT: ...
|
|
||||||
|
|
||||||
|
|
||||||
class _SsoSettingsMappingRow(Protocol):
|
class _SsoSettingsMappingRow(Protocol):
|
||||||
@property
|
@property
|
||||||
def sso_settings(self) -> Mapping[str, object] | None: ...
|
def sso_settings(self) -> Mapping[str, object] | None: ...
|
||||||
|
|
||||||
|
|
||||||
class _HasSsoSettingsMappingTable(Protocol):
|
def _sso_settings_mapping_db(repo: SSOConfigRepository) -> TableActions[_SsoSettingsMappingRow]:
|
||||||
@property
|
return cast( # cast-ok: prisma types Json columns as str; the client hands back the deserialized value
|
||||||
def table(self) -> _PrismaTableActions[_SsoSettingsMappingRow]: ...
|
"TableActions[_SsoSettingsMappingRow]", repo.table
|
||||||
|
)
|
||||||
|
|
||||||
def _sso_settings_mapping_db(repo: _HasSsoSettingsMappingTable) -> _PrismaTableActions[_SsoSettingsMappingRow]:
|
|
||||||
return repo.table
|
|
||||||
|
|
||||||
|
|
||||||
class _StoredSsoSettingsRow(Protocol):
|
class _StoredSsoSettingsRow(Protocol):
|
||||||
|
|
@ -67,12 +60,7 @@ class _StoredSsoSettingsRow(Protocol):
|
||||||
def sso_settings(self) -> object: ...
|
def sso_settings(self) -> object: ...
|
||||||
|
|
||||||
|
|
||||||
class _HasStoredSsoSettingsTable(Protocol):
|
def _stored_sso_settings_db(repo: SSOConfigRepository) -> TableActions[_StoredSsoSettingsRow]:
|
||||||
@property
|
|
||||||
def table(self) -> _PrismaTableActions[_StoredSsoSettingsRow]: ...
|
|
||||||
|
|
||||||
|
|
||||||
def _stored_sso_settings_db(repo: _HasStoredSsoSettingsTable) -> _PrismaTableActions[_StoredSsoSettingsRow]:
|
|
||||||
return repo.table
|
return repo.table
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -81,13 +69,10 @@ class _UiSettingsRow(Protocol):
|
||||||
def ui_settings(self) -> str | Mapping[str, JsonValue] | None: ...
|
def ui_settings(self) -> str | Mapping[str, JsonValue] | None: ...
|
||||||
|
|
||||||
|
|
||||||
class _HasUiSettingsTable(Protocol):
|
def _ui_settings_db(repo: UISettingsRepository) -> TableActions[_UiSettingsRow]:
|
||||||
@property
|
return cast( # cast-ok: prisma types Json columns as str; the client hands back the deserialized value
|
||||||
def table(self) -> _PrismaTableActions[_UiSettingsRow]: ...
|
"TableActions[_UiSettingsRow]", repo.table
|
||||||
|
)
|
||||||
|
|
||||||
def _ui_settings_db(repo: _HasUiSettingsTable) -> _PrismaTableActions[_UiSettingsRow]:
|
|
||||||
return repo.table
|
|
||||||
|
|
||||||
|
|
||||||
class _ConfigParamRow(Protocol):
|
class _ConfigParamRow(Protocol):
|
||||||
|
|
@ -95,13 +80,10 @@ class _ConfigParamRow(Protocol):
|
||||||
def param_value(self) -> str | Mapping[str, object] | None: ...
|
def param_value(self) -> str | Mapping[str, object] | None: ...
|
||||||
|
|
||||||
|
|
||||||
class _HasConfigParamTable(Protocol):
|
def _config_param_db(repo: ConfigRepository) -> TableActions[_ConfigParamRow]:
|
||||||
@property
|
return cast( # cast-ok: prisma's LiteLLM_Config actions object, whose Json column parses to a mapping
|
||||||
def table(self) -> _PrismaTableActions[_ConfigParamRow]: ...
|
"TableActions[_ConfigParamRow]", repo.table
|
||||||
|
)
|
||||||
|
|
||||||
def _config_param_db(repo: _HasConfigParamTable) -> _PrismaTableActions[_ConfigParamRow]:
|
|
||||||
return repo.table
|
|
||||||
|
|
||||||
|
|
||||||
# Maps each UIThemeConfig field to the env var the UI branding path reads it
|
# Maps each UIThemeConfig field to the env var the UI branding path reads it
|
||||||
|
|
|
||||||
|
|
@ -177,6 +177,7 @@ from litellm.types.utils import LLMResponseTypes, LoggedLiteLLMParams
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from mcp.types import CallToolResult
|
from mcp.types import CallToolResult
|
||||||
from opentelemetry.trace import Span as _Span
|
from opentelemetry.trace import Span as _Span
|
||||||
|
from prisma import models as prisma_models
|
||||||
from prisma.actions import LiteLLM_DeprecatedVerificationTokenActions
|
from prisma.actions import LiteLLM_DeprecatedVerificationTokenActions
|
||||||
from prisma.client import TransactionManager
|
from prisma.client import TransactionManager
|
||||||
from prisma.models import LiteLLM_DeprecatedVerificationToken
|
from prisma.models import LiteLLM_DeprecatedVerificationToken
|
||||||
|
|
@ -186,6 +187,7 @@ if TYPE_CHECKING:
|
||||||
from litellm.models.team import LiteLLM_TeamTableCachedObj
|
from litellm.models.team import LiteLLM_TeamTableCachedObj
|
||||||
from litellm.proxy.db.autorouter_session_rollup import AutoRouterTurnTransaction
|
from litellm.proxy.db.autorouter_session_rollup import AutoRouterTurnTransaction
|
||||||
from litellm.proxy.db.spend_log_tool_index import ToolUsageTransaction
|
from litellm.proxy.db.spend_log_tool_index import ToolUsageTransaction
|
||||||
|
from litellm.repositories.prisma_protocols import TableActions
|
||||||
from litellm.types.proxy.policy_engine.pipeline_types import GuardrailPipeline
|
from litellm.types.proxy.policy_engine.pipeline_types import GuardrailPipeline
|
||||||
|
|
||||||
Span = _Span | object
|
Span = _Span | object
|
||||||
|
|
@ -3282,7 +3284,10 @@ async def prefetch_config_params(prisma_client: "PrismaClient | None", param_nam
|
||||||
if not param_names:
|
if not param_names:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
rows: Final = await ConfigRepository(prisma_client).table.find_many(where={"param_name": {"in": param_names}})
|
config_table: Final = cast( # cast-ok: ConfigRepository.table is prisma's litellm_config actions object
|
||||||
|
"TableActions[prisma_models.LiteLLM_Config]", ConfigRepository(prisma_client).table
|
||||||
|
)
|
||||||
|
rows: Final = await config_table.find_many(where={"param_name": {"in": param_names}})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
verbose_proxy_logger.debug(
|
verbose_proxy_logger.debug(
|
||||||
"prefetch_config_params failed, falling through to per-param queries: %s",
|
"prefetch_config_params failed, falling through to per-param queries: %s",
|
||||||
|
|
@ -3571,8 +3576,8 @@ class PrismaClient:
|
||||||
|
|
||||||
return hashed_token
|
return hashed_token
|
||||||
|
|
||||||
def jsonify_object(self, data: dict) -> dict:
|
def jsonify_object(self, data: Mapping[str, object]) -> dict[str, object]:
|
||||||
db_data: Final = copy.deepcopy(data)
|
db_data: Final[dict[str, object]] = copy.deepcopy(dict(data))
|
||||||
|
|
||||||
for k, v in db_data.items():
|
for k, v in db_data.items():
|
||||||
if isinstance(v, dict):
|
if isinstance(v, dict):
|
||||||
|
|
@ -3706,7 +3711,10 @@ class PrismaClient:
|
||||||
elif table_name == "keys":
|
elif table_name == "keys":
|
||||||
return await VerificationTokenRepository(self).table.find_first(where={key: value})
|
return await VerificationTokenRepository(self).table.find_first(where={key: value})
|
||||||
elif table_name == "config":
|
elif table_name == "config":
|
||||||
return await ConfigRepository(self).table.find_first(where={key: value})
|
config_table: Final = cast( # cast-ok: ConfigRepository.table is prisma's litellm_config actions object
|
||||||
|
"TableActions[prisma_models.LiteLLM_Config]", ConfigRepository(self).table
|
||||||
|
)
|
||||||
|
return await config_table.find_first(where={key: value})
|
||||||
elif table_name == "spend":
|
elif table_name == "spend":
|
||||||
return await self.db.l.find_first(where={key: value})
|
return await self.db.l.find_first(where={key: value})
|
||||||
return None
|
return None
|
||||||
|
|
@ -3809,9 +3817,9 @@ class PrismaClient:
|
||||||
self,
|
self,
|
||||||
token: str | list | None = None,
|
token: str | list | None = None,
|
||||||
user_id: str | None = None,
|
user_id: str | None = None,
|
||||||
user_id_list: list | None = None,
|
user_id_list: Sequence[str] | None = None,
|
||||||
team_id: str | None = None,
|
team_id: str | None = None,
|
||||||
team_id_list: list | None = None,
|
team_id_list: Sequence[str] | None = None,
|
||||||
key_val: dict | None = None,
|
key_val: dict | None = None,
|
||||||
table_name: Literal[
|
table_name: Literal[
|
||||||
"user", "key", "config", "spend", "enduser", "budget", "team", "user_notification", "combined_view"
|
"user", "key", "config", "spend", "enduser", "budget", "team", "user_notification", "combined_view"
|
||||||
|
|
@ -3894,14 +3902,14 @@ class PrismaClient:
|
||||||
if isinstance(r.expires, datetime):
|
if isinstance(r.expires, datetime):
|
||||||
r.expires = r.expires.isoformat()
|
r.expires = r.expires.isoformat()
|
||||||
elif query_type == "find_all":
|
elif query_type == "find_all":
|
||||||
where_filter: Final[dict] = {}
|
where_filter: Final[dict[str, dict[str, Sequence[str]]]] = {}
|
||||||
if token is not None:
|
if token is not None:
|
||||||
where_filter["token"] = {}
|
where_filter["token"] = {}
|
||||||
if isinstance(token, str):
|
if isinstance(token, str):
|
||||||
token = _hash_token_if_needed(token=token)
|
token = _hash_token_if_needed(token=token)
|
||||||
where_filter["token"]["in"] = [token]
|
where_filter["token"]["in"] = [token]
|
||||||
elif isinstance(token, list):
|
elif isinstance(token, list):
|
||||||
hashed_tokens: Final = []
|
hashed_tokens: Final[list[str]] = []
|
||||||
for t in token:
|
for t in token:
|
||||||
assert isinstance(t, str)
|
assert isinstance(t, str)
|
||||||
if t.startswith("sk-"):
|
if t.startswith("sk-"):
|
||||||
|
|
@ -4198,7 +4206,7 @@ class PrismaClient:
|
||||||
)
|
)
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
def jsonify_team_object(self, db_data: dict):
|
def jsonify_team_object(self, db_data: Mapping[str, object]) -> dict[str, object]:
|
||||||
db_data = self.jsonify_object(data=db_data)
|
db_data = self.jsonify_object(data=db_data)
|
||||||
if db_data.get("members_with_roles", None) is not None and isinstance(db_data["members_with_roles"], list):
|
if db_data.get("members_with_roles", None) is not None and isinstance(db_data["members_with_roles"], list):
|
||||||
db_data["members_with_roles"] = json.dumps(db_data["members_with_roles"])
|
db_data["members_with_roles"] = json.dumps(db_data["members_with_roles"])
|
||||||
|
|
@ -4216,7 +4224,7 @@ class PrismaClient:
|
||||||
)
|
)
|
||||||
async def insert_data(
|
async def insert_data(
|
||||||
self,
|
self,
|
||||||
data: dict,
|
data: Mapping[str, object],
|
||||||
table_name: Literal["user", "key", "config", "spend", "team", "user_notification"],
|
table_name: Literal["user", "key", "config", "spend", "team", "user_notification"],
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
|
|
@ -4226,10 +4234,12 @@ class PrismaClient:
|
||||||
try:
|
try:
|
||||||
verbose_proxy_logger.debug(
|
verbose_proxy_logger.debug(
|
||||||
"PrismaClient: insert_data: %s",
|
"PrismaClient: insert_data: %s",
|
||||||
{**data, "token": self.hash_token(token=data["token"])} if data.get("token") is not None else data,
|
{**data, "token": self.hash_token(token=cast("str", data["token"]))} # cast-ok: a key token is a str
|
||||||
|
if data.get("token") is not None
|
||||||
|
else data,
|
||||||
)
|
)
|
||||||
if table_name == "key":
|
if table_name == "key":
|
||||||
token: Final = data["token"]
|
token: Final = cast("str", data["token"]) # cast-ok: the key table's token column is a str
|
||||||
hashed_token: Final = self.hash_token(token=token)
|
hashed_token: Final = self.hash_token(token=token)
|
||||||
db_data = self.jsonify_object(data=data)
|
db_data = self.jsonify_object(data=data)
|
||||||
db_data["token"] = hashed_token
|
db_data["token"] = hashed_token
|
||||||
|
|
@ -4364,14 +4374,14 @@ class PrismaClient:
|
||||||
async def update_data(
|
async def update_data(
|
||||||
self,
|
self,
|
||||||
token: str | None = None,
|
token: str | None = None,
|
||||||
data: dict = {},
|
data: Mapping[str, object] = {},
|
||||||
data_list: list | None = None,
|
data_list: list | None = None,
|
||||||
user_id: str | None = None,
|
user_id: str | None = None,
|
||||||
team_id: str | None = None,
|
team_id: str | None = None,
|
||||||
query_type: Literal["update", "update_many"] = "update",
|
query_type: Literal["update", "update_many"] = "update",
|
||||||
table_name: Literal["user", "key", "config", "spend", "team", "enduser", "budget"] | None = None,
|
table_name: Literal["user", "key", "config", "spend", "team", "enduser", "budget"] | None = None,
|
||||||
update_key_values: dict | None = None,
|
update_key_values: dict[str, object] | None = None,
|
||||||
update_key_values_custom_query: dict | None = None,
|
update_key_values_custom_query: dict[str, object] | None = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Update existing data
|
Update existing data
|
||||||
|
|
@ -4397,14 +4407,14 @@ class PrismaClient:
|
||||||
try:
|
try:
|
||||||
_data = response.model_dump()
|
_data = response.model_dump()
|
||||||
except Exception:
|
except Exception:
|
||||||
_data = response.dict()
|
_data = response.dict() # pyright: ignore[reportDeprecated] # pydantic-v1 row fallback
|
||||||
return {"token": token, "data": _data}
|
return {"token": token, "data": _data}
|
||||||
elif user_id is not None or (table_name is not None and table_name == "user") and query_type == "update":
|
elif user_id is not None or (table_name is not None and table_name == "user") and query_type == "update":
|
||||||
"""
|
"""
|
||||||
If data['spend'] + data['user'], update the user table with spend info as well
|
If data['spend'] + data['user'], update the user table with spend info as well
|
||||||
"""
|
"""
|
||||||
if user_id is None:
|
if user_id is None:
|
||||||
user_id = db_data["user_id"]
|
user_id = cast("str", db_data["user_id"]) # cast-ok: the user table's user_id column is a str
|
||||||
if update_key_values is None:
|
if update_key_values is None:
|
||||||
if update_key_values_custom_query is not None:
|
if update_key_values_custom_query is not None:
|
||||||
update_key_values = update_key_values_custom_query
|
update_key_values = update_key_values_custom_query
|
||||||
|
|
@ -4426,7 +4436,7 @@ class PrismaClient:
|
||||||
If data['spend'] + data['user'], update the user table with spend info as well
|
If data['spend'] + data['user'], update the user table with spend info as well
|
||||||
"""
|
"""
|
||||||
if team_id is None:
|
if team_id is None:
|
||||||
team_id = db_data["team_id"]
|
team_id = cast("str | None", db_data["team_id"]) # cast-ok: team_id column is a nullable str
|
||||||
if update_key_values is None:
|
if update_key_values is None:
|
||||||
update_key_values = db_data
|
update_key_values = db_data
|
||||||
if "team_id" not in db_data and team_id is not None:
|
if "team_id" not in db_data and team_id is not None:
|
||||||
|
|
@ -4600,8 +4610,8 @@ class PrismaClient:
|
||||||
)
|
)
|
||||||
async def delete_data(
|
async def delete_data(
|
||||||
self,
|
self,
|
||||||
tokens: list | None = None,
|
tokens: Sequence[str | None] | None = None,
|
||||||
team_id_list: list | None = None,
|
team_id_list: Sequence[str] | None = None,
|
||||||
table_name: Literal["user", "key", "config", "spend", "team"] | None = None,
|
table_name: Literal["user", "key", "config", "spend", "team"] | None = None,
|
||||||
user_id: str | None = None,
|
user_id: str | None = None,
|
||||||
):
|
):
|
||||||
|
|
@ -4613,14 +4623,14 @@ class PrismaClient:
|
||||||
start_time: Final = time.time()
|
start_time: Final = time.time()
|
||||||
try:
|
try:
|
||||||
if tokens is not None and isinstance(tokens, list):
|
if tokens is not None and isinstance(tokens, list):
|
||||||
hashed_tokens: Final = []
|
hashed_tokens: Final[list[str | None]] = []
|
||||||
for token in tokens:
|
for token in tokens:
|
||||||
if isinstance(token, str) and token.startswith("sk-"):
|
if isinstance(token, str) and token.startswith("sk-"):
|
||||||
hashed_token = self.hash_token(token=token)
|
hashed_token = self.hash_token(token=token)
|
||||||
else:
|
else:
|
||||||
hashed_token = token
|
hashed_token = token
|
||||||
hashed_tokens.append(hashed_token)
|
hashed_tokens.append(hashed_token)
|
||||||
filter_query: dict = {}
|
filter_query: dict[str, object] = {}
|
||||||
if user_id is not None:
|
if user_id is not None:
|
||||||
filter_query = {"AND": [{"token": {"in": hashed_tokens}}, {"user_id": user_id}]}
|
filter_query = {"AND": [{"token": {"in": hashed_tokens}}, {"user_id": user_id}]}
|
||||||
else:
|
else:
|
||||||
|
|
@ -5765,12 +5775,12 @@ class PrismaClient:
|
||||||
limit: int = 100,
|
limit: int = 100,
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
status_filter: str | None = None,
|
status_filter: str | None = None,
|
||||||
):
|
) -> "Sequence[prisma_models.LiteLLM_HealthCheckTable]":
|
||||||
"""
|
"""
|
||||||
Get health check history with optional filtering
|
Get health check history with optional filtering
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
where_clause: Final = {}
|
where_clause: Final[dict[str, str]] = {}
|
||||||
if model_name:
|
if model_name:
|
||||||
where_clause["model_name"] = model_name
|
where_clause["model_name"] = model_name
|
||||||
if status_filter:
|
if status_filter:
|
||||||
|
|
@ -5787,7 +5797,7 @@ class PrismaClient:
|
||||||
verbose_proxy_logger.error("Error getting health check history: %s", e)
|
verbose_proxy_logger.error("Error getting health check history: %s", e)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
async def get_all_latest_health_checks(self):
|
async def get_all_latest_health_checks(self) -> "Sequence[prisma_models.LiteLLM_HealthCheckTable]":
|
||||||
"""
|
"""
|
||||||
Get the latest health check for each model.
|
Get the latest health check for each model.
|
||||||
|
|
||||||
|
|
@ -5965,15 +5975,17 @@ async def migrate_passwords_to_scrypt_async(prisma_client) -> str:
|
||||||
return len(s) == 64 and all(c in "0123456789abcdef" for c in s)
|
return len(s) == 64 and all(c in "0123456789abcdef" for c in s)
|
||||||
|
|
||||||
plaintext_users: Final = [
|
plaintext_users: Final = [
|
||||||
u for u in all_with_pw if u.password and not u.password.startswith("scrypt:") and not _is_sha256_hex(u.password)
|
(u.user_id, u.password)
|
||||||
|
for u in all_with_pw
|
||||||
|
if u.password and not u.password.startswith("scrypt:") and not _is_sha256_hex(u.password)
|
||||||
]
|
]
|
||||||
if not plaintext_users:
|
if not plaintext_users:
|
||||||
return "No plaintext passwords found"
|
return "No plaintext passwords found"
|
||||||
|
|
||||||
for user in plaintext_users:
|
for user_id, plaintext_password in plaintext_users:
|
||||||
await UserRepository(prisma_client).table.update(
|
await UserRepository(prisma_client).table.update(
|
||||||
where={"user_id": user.user_id},
|
where={"user_id": user_id},
|
||||||
data={"password": hash_password(user.password)},
|
data={"password": hash_password(plaintext_password)},
|
||||||
)
|
)
|
||||||
return f"Migrated {len(plaintext_users)} plaintext passwords to scrypt"
|
return f"Migrated {len(plaintext_users)} plaintext passwords to scrypt"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,9 @@
|
||||||
from typing import Annotated, Any, Final
|
from typing import (
|
||||||
|
Annotated,
|
||||||
|
Any, # noqa: TID251 # jsonify_object in proxy/utils.py is annotated with a bare dict
|
||||||
|
Final,
|
||||||
|
cast, # noqa: TID251 # jsonify_object in proxy/utils.py is annotated with a bare dict
|
||||||
|
)
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
||||||
|
|
||||||
|
|
@ -591,7 +596,11 @@ async def index_create(
|
||||||
index_data: Final = index_create_request.model_dump(exclude_none=True)
|
index_data: Final = index_create_request.model_dump(exclude_none=True)
|
||||||
index_data["created_by"] = user_api_key_dict.user_id
|
index_data["created_by"] = user_api_key_dict.user_id
|
||||||
index_data["updated_by"] = user_api_key_dict.user_id
|
index_data["updated_by"] = user_api_key_dict.user_id
|
||||||
new_index = await ManagedVectorStoreIndexRepository(prisma_client).table.create(data=jsonify_object(index_data))
|
new_index = await ManagedVectorStoreIndexRepository(prisma_client).table.create(
|
||||||
|
data=cast( # cast-ok: jsonify_object deep-copies a model_dump, so keys are str and values plain objects
|
||||||
|
"dict[str, object]", jsonify_object(index_data)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return new_index.model_dump()
|
return new_index.model_dump()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,7 @@ All /vector_store management endpoints
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
import json
|
import json
|
||||||
from collections.abc import Mapping
|
from typing import TYPE_CHECKING, Any, Final
|
||||||
from typing import TYPE_CHECKING, Any, Final, Protocol
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
|
||||||
|
|
@ -37,6 +36,7 @@ from litellm.proxy.common_utils.encrypt_decrypt_utils import decrypt_value_helpe
|
||||||
from litellm.proxy.common_utils.rbac_utils import check_feature_access_for_user
|
from litellm.proxy.common_utils.rbac_utils import check_feature_access_for_user
|
||||||
from litellm.proxy.vector_store_endpoints.utils import can_user_access_vector_store
|
from litellm.proxy.vector_store_endpoints.utils import can_user_access_vector_store
|
||||||
from litellm.repositories.model_repository import ModelRepository
|
from litellm.repositories.model_repository import ModelRepository
|
||||||
|
from litellm.repositories.prisma_protocols import TableActions
|
||||||
from litellm.repositories.table_repositories import ManagedVectorStoresRepository
|
from litellm.repositories.table_repositories import ManagedVectorStoresRepository
|
||||||
from litellm.secret_managers.main import get_secret
|
from litellm.secret_managers.main import get_secret
|
||||||
from litellm.types.vector_stores import (
|
from litellm.types.vector_stores import (
|
||||||
|
|
@ -51,17 +51,7 @@ from litellm.vector_stores.vector_store_registry import VectorStoreRegistry
|
||||||
router: Final = APIRouter()
|
router: Final = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
class _VectorStoreTableActions(Protocol):
|
def _vector_store_table(prisma_client: "PrismaClient") -> "TableActions[_VectorStoreRow]":
|
||||||
async def find_unique(self, where: Mapping[str, str]) -> "_VectorStoreRow | None": ...
|
|
||||||
|
|
||||||
async def create(self, data: Mapping[str, object]) -> "_VectorStoreRow": ...
|
|
||||||
|
|
||||||
async def update(self, where: Mapping[str, str], data: Mapping[str, object]) -> "_VectorStoreRow": ...
|
|
||||||
|
|
||||||
async def delete(self, where: Mapping[str, str]) -> "_VectorStoreRow | None": ...
|
|
||||||
|
|
||||||
|
|
||||||
def _vector_store_table(prisma_client: "PrismaClient") -> _VectorStoreTableActions:
|
|
||||||
return ManagedVectorStoresRepository(prisma_client).table
|
return ManagedVectorStoresRepository(prisma_client).table
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -277,7 +267,7 @@ async def _resolve_embedding_config_from_db(
|
||||||
if db_model and db_model.litellm_params:
|
if db_model and db_model.litellm_params:
|
||||||
# Extract litellm_params (could be dict or JSON string)
|
# Extract litellm_params (could be dict or JSON string)
|
||||||
model_params = db_model.litellm_params
|
model_params = db_model.litellm_params
|
||||||
if isinstance(model_params, str):
|
if isinstance(model_params, str): # pyright: ignore[reportUnnecessaryIsInstance] # prisma Json is str
|
||||||
model_params = json.loads(model_params)
|
model_params = json.loads(model_params)
|
||||||
|
|
||||||
# Decrypt values from database (similar to how proxy_server.py does it)
|
# Decrypt values from database (similar to how proxy_server.py does it)
|
||||||
|
|
@ -888,6 +878,12 @@ async def update_vector_store(
|
||||||
data=update_data,
|
data=update_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if updated is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail=f"Vector store with ID {vector_store_id} not found",
|
||||||
|
)
|
||||||
|
|
||||||
updated_vs: Final = _row_to_vector_store(updated)
|
updated_vs: Final = _row_to_vector_store(updated)
|
||||||
|
|
||||||
# Immediately update in-memory registry to keep it in sync
|
# Immediately update in-memory registry to keep it in sync
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ from typing import Any, Final, Generic, Protocol, TypeVar, runtime_checkable
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from litellm.repositories.prisma_protocols import TableActions
|
||||||
|
|
||||||
T = TypeVar("T", bound=BaseModel)
|
T = TypeVar("T", bound=BaseModel)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -49,7 +51,7 @@ class BaseRepository(ABC, Generic[T]):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def table(self) -> Any: # any-ok: Prisma table actions are reached through the untyped client wrapper
|
def table(self) -> TableActions[DbRecord]:
|
||||||
"""Return the Prisma table for this repository."""
|
"""Return the Prisma table for this repository."""
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
@ -76,33 +78,28 @@ class BaseRepository(ABC, Generic[T]):
|
||||||
|
|
||||||
async def find_many(
|
async def find_many(
|
||||||
self,
|
self,
|
||||||
where: dict[str, Any] | None = None,
|
where: Mapping[str, object] | None = None,
|
||||||
skip: int | None = None,
|
skip: int | None = None,
|
||||||
take: int | None = None,
|
take: int | None = None,
|
||||||
order: dict[str, str] | None = None,
|
order: Mapping[str, str] | None = None,
|
||||||
) -> list[T]:
|
) -> list[T]:
|
||||||
"""Find multiple records matching the criteria."""
|
"""Find multiple records matching the criteria."""
|
||||||
kwargs: Final[dict[str, Any]] = {}
|
records: Final = await self.table.find_many(
|
||||||
if where:
|
take=take,
|
||||||
kwargs["where"] = where
|
skip=skip,
|
||||||
if skip is not None:
|
where=where or None,
|
||||||
kwargs["skip"] = skip
|
order=order or None,
|
||||||
if take is not None:
|
)
|
||||||
kwargs["take"] = take
|
|
||||||
if order:
|
|
||||||
kwargs["order"] = order
|
|
||||||
|
|
||||||
records: Final = await self.table.find_many(**kwargs)
|
|
||||||
return self._to_model_list(records)
|
return self._to_model_list(records)
|
||||||
|
|
||||||
async def create(self, data: dict[str, Any]) -> T:
|
async def create(self, data: Mapping[str, object]) -> T:
|
||||||
"""Create a new record."""
|
"""Create a new record."""
|
||||||
record: Final = await self.table.create(data=data)
|
record: Final = await self.table.create(data=data)
|
||||||
model: Final = self._to_model(record)
|
model: Final = self._to_model(record)
|
||||||
assert model is not None
|
assert model is not None
|
||||||
return model
|
return model
|
||||||
|
|
||||||
async def update(self, id_value: str, data: dict[str, Any], id_field: str = "id") -> T | None:
|
async def update(self, id_value: str, data: Mapping[str, object], id_field: str = "id") -> T | None:
|
||||||
"""Update an existing record."""
|
"""Update an existing record."""
|
||||||
record: Final = await self.table.update(where={id_field: id_value}, data=data)
|
record: Final = await self.table.update(where={id_field: id_value}, data=data)
|
||||||
return self._to_model(record)
|
return self._to_model(record)
|
||||||
|
|
@ -112,7 +109,7 @@ class BaseRepository(ABC, Generic[T]):
|
||||||
record: Final = await self.table.delete(where={id_field: id_value})
|
record: Final = await self.table.delete(where={id_field: id_value})
|
||||||
return self._to_model(record)
|
return self._to_model(record)
|
||||||
|
|
||||||
async def count(self, where: dict[str, Any] | None = None) -> int:
|
async def count(self, where: Mapping[str, object] | None = None) -> int:
|
||||||
"""Count records matching the criteria."""
|
"""Count records matching the criteria."""
|
||||||
return await self.table.count(where=where)
|
return await self.table.count(where=where)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,21 @@
|
||||||
Budget repository for database operations on LiteLLM_BudgetTable.
|
Budget repository for database operations on LiteLLM_BudgetTable.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Any, Final
|
from typing import TYPE_CHECKING, Any, Final
|
||||||
|
|
||||||
from litellm.models.budget import LiteLLM_BudgetTable
|
from litellm.models.budget import LiteLLM_BudgetTable
|
||||||
from litellm.repositories.base_repository import BaseRepository
|
from litellm.repositories.base_repository import BaseRepository
|
||||||
|
from litellm.repositories.prisma_protocols import TableActions
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from prisma import models as prisma_models
|
||||||
|
|
||||||
|
|
||||||
class BudgetRepository(BaseRepository[LiteLLM_BudgetTable]):
|
class BudgetRepository(BaseRepository[LiteLLM_BudgetTable]):
|
||||||
"""Repository for budget database operations."""
|
"""Repository for budget database operations."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def table(self) -> Any:
|
def table(self) -> TableActions["prisma_models.LiteLLM_BudgetTable"]:
|
||||||
return self.prisma_client.db.litellm_budgettable
|
return self.prisma_client.db.litellm_budgettable
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ class ConfigRepository:
|
||||||
return self.prisma_client.db.litellm_config
|
return self.prisma_client.db.litellm_config
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def table(self) -> Any:
|
def table(self) -> _ConfigTable:
|
||||||
return self._config_table
|
return self._config_table
|
||||||
|
|
||||||
async def get_param(self, param_name: str) -> ConfigParam | None:
|
async def get_param(self, param_name: str) -> ConfigParam | None:
|
||||||
|
|
|
||||||
|
|
@ -6,54 +6,77 @@ credential values is the caller's responsibility (see ``CredentialHelperUtils``)
|
||||||
so reads return the stored values verbatim.
|
so reads return the stored values verbatim.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Any, Final
|
from collections.abc import Mapping, Sequence
|
||||||
|
from typing import TYPE_CHECKING, Any, Final, Protocol, TypeAlias
|
||||||
|
|
||||||
from litellm.models.credentials import CredentialItem
|
from litellm.models.credentials import CredentialItem
|
||||||
from litellm.proxy.common_utils.config_sync_pubsub import wrap_table_actions_for_config_sync
|
from litellm.proxy.common_utils.config_sync_pubsub import wrap_table_actions_for_config_sync
|
||||||
|
from litellm.repositories.base_repository import DbRecord, record_to_dict
|
||||||
|
from litellm.repositories.prisma_protocols import TableActions
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from prisma import models as prisma_models
|
||||||
|
|
||||||
|
_CredentialsTable: TypeAlias = TableActions[prisma_models.LiteLLM_CredentialsTable]
|
||||||
|
|
||||||
|
|
||||||
|
class _PrismaCredentialsDb(Protocol):
|
||||||
|
@property
|
||||||
|
def litellm_credentialstable(self) -> "_CredentialsTable": ...
|
||||||
|
|
||||||
|
|
||||||
|
class _PrismaClientView(Protocol):
|
||||||
|
@property
|
||||||
|
def db(self) -> _PrismaCredentialsDb: ...
|
||||||
|
|
||||||
|
|
||||||
class CredentialsRepository:
|
class CredentialsRepository:
|
||||||
"""Repository for credentials database operations, keyed by credential name."""
|
"""Repository for credentials database operations, keyed by credential name."""
|
||||||
|
|
||||||
def __init__(self, prisma_client: Any):
|
def __init__(self, prisma_client: Any): # any-ok: PrismaClient is an untyped runtime wrapper
|
||||||
self._prisma_client = prisma_client
|
self._prisma_client = prisma_client
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def prisma_client(self) -> Any:
|
def prisma_client(self) -> _PrismaClientView:
|
||||||
if self._prisma_client is None:
|
if self._prisma_client is None:
|
||||||
raise RuntimeError("No DB Connected. See - https://docs.litellm.ai/docs/proxy/virtual_keys")
|
raise RuntimeError("No DB Connected. See - https://docs.litellm.ai/docs/proxy/virtual_keys")
|
||||||
return self._prisma_client
|
client: Final[_PrismaClientView] = self._prisma_client
|
||||||
|
return client
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def table(self) -> Any:
|
def table(self) -> "_CredentialsTable":
|
||||||
return wrap_table_actions_for_config_sync(
|
return wrap_table_actions_for_config_sync(
|
||||||
actions=self.prisma_client.db.litellm_credentialstable,
|
actions=self.prisma_client.db.litellm_credentialstable,
|
||||||
table_name="litellm_credentialstable",
|
table_name="litellm_credentialstable",
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _to_model(record: Any) -> CredentialItem | None:
|
def _to_model(record: DbRecord | None) -> CredentialItem | None:
|
||||||
if record is None:
|
if record is None:
|
||||||
return None
|
return None
|
||||||
data: Final = record.dict() if hasattr(record, "dict") else dict(record)
|
data: Final = record_to_dict(record)
|
||||||
return CredentialItem(
|
return CredentialItem.model_validate(
|
||||||
credential_name=data["credential_name"],
|
{
|
||||||
credential_values=data.get("credential_values") or {},
|
"credential_name": data["credential_name"],
|
||||||
credential_info=data.get("credential_info") or {},
|
"credential_values": data.get("credential_values") or {},
|
||||||
|
"credential_info": data.get("credential_info") or {},
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
async def find_all(self) -> Any:
|
async def find_all(self) -> Sequence["prisma_models.LiteLLM_CredentialsTable"]:
|
||||||
return await self.table.find_many()
|
return await self.table.find_many()
|
||||||
|
|
||||||
async def create(self, data: dict[str, Any]) -> Any:
|
async def create(self, data: Mapping[str, object]) -> "prisma_models.LiteLLM_CredentialsTable":
|
||||||
return await self.table.create(data=data)
|
return await self.table.create(data=data)
|
||||||
|
|
||||||
async def find_by_name(self, credential_name: str) -> CredentialItem | None:
|
async def find_by_name(self, credential_name: str) -> CredentialItem | None:
|
||||||
record: Final = await self.table.find_unique(where={"credential_name": credential_name})
|
record: Final = await self.table.find_unique(where={"credential_name": credential_name})
|
||||||
return self._to_model(record)
|
return self._to_model(record)
|
||||||
|
|
||||||
async def update_by_name(self, credential_name: str, data: dict[str, Any]) -> Any:
|
async def update_by_name(
|
||||||
|
self, credential_name: str, data: Mapping[str, object]
|
||||||
|
) -> "prisma_models.LiteLLM_CredentialsTable | None":
|
||||||
return await self.table.update(where={"credential_name": credential_name}, data=data)
|
return await self.table.update(where={"credential_name": credential_name}, data=data)
|
||||||
|
|
||||||
async def delete_by_name(self, credential_name: str) -> Any:
|
async def delete_by_name(self, credential_name: str) -> "prisma_models.LiteLLM_CredentialsTable | None":
|
||||||
return await self.table.delete(where={"credential_name": credential_name})
|
return await self.table.delete(where={"credential_name": credential_name})
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@ Model repository for database operations on LiteLLM_ProxyModelTable.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from collections.abc import Awaitable, Mapping, Sequence
|
from collections.abc import Mapping
|
||||||
from typing import Any, Final, Protocol
|
from typing import TYPE_CHECKING, Any, Final, Protocol
|
||||||
|
|
||||||
from litellm.models.model import LiteLLM_ProxyModelTable
|
from litellm.models.model import LiteLLM_ProxyModelTable
|
||||||
from litellm.proxy.common_utils.config_sync_pubsub import wrap_table_actions_for_config_sync
|
from litellm.proxy.common_utils.config_sync_pubsub import wrap_table_actions_for_config_sync
|
||||||
|
|
@ -12,25 +12,21 @@ from litellm.proxy.common_utils.encrypt_decrypt_utils import (
|
||||||
decrypt_value_helper,
|
decrypt_value_helper,
|
||||||
encrypt_value_helper,
|
encrypt_value_helper,
|
||||||
)
|
)
|
||||||
from litellm.repositories.base_repository import BaseRepository, DbRecord
|
from litellm.repositories.base_repository import BaseRepository
|
||||||
|
from litellm.repositories.prisma_protocols import TableActions
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from prisma import models as prisma_models
|
||||||
|
|
||||||
|
|
||||||
class _PrismaModelDb(Protocol):
|
class _PrismaModelDb(Protocol):
|
||||||
litellm_proxymodeltable: object
|
@property
|
||||||
|
def litellm_proxymodeltable(self) -> TableActions["prisma_models.LiteLLM_ProxyModelTable"]: ...
|
||||||
|
|
||||||
|
|
||||||
class _PrismaClientView(Protocol):
|
class _PrismaClientView(Protocol):
|
||||||
db: _PrismaModelDb
|
@property
|
||||||
|
def db(self) -> _PrismaModelDb: ...
|
||||||
|
|
||||||
class _ProxyModelActions(Protocol):
|
|
||||||
"""Prisma table actions used by :class:`ModelRepository`."""
|
|
||||||
|
|
||||||
def find_many(self, *, where: Mapping[str, object] | None = None) -> Awaitable[Sequence[DbRecord]]: ...
|
|
||||||
|
|
||||||
def create(self, *, data: Mapping[str, object]) -> Awaitable[DbRecord]: ...
|
|
||||||
|
|
||||||
def update(self, *, where: Mapping[str, object], data: Mapping[str, object]) -> Awaitable[DbRecord | None]: ...
|
|
||||||
|
|
||||||
|
|
||||||
class ModelRepository(BaseRepository[LiteLLM_ProxyModelTable]):
|
class ModelRepository(BaseRepository[LiteLLM_ProxyModelTable]):
|
||||||
|
|
@ -41,17 +37,13 @@ class ModelRepository(BaseRepository[LiteLLM_ProxyModelTable]):
|
||||||
self._encryption_key = encryption_key
|
self._encryption_key = encryption_key
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def table(self) -> Any:
|
def table(self) -> TableActions["prisma_models.LiteLLM_ProxyModelTable"]:
|
||||||
client: Final[_PrismaClientView] = self.prisma_client
|
client: Final[_PrismaClientView] = self.prisma_client
|
||||||
return wrap_table_actions_for_config_sync(
|
return wrap_table_actions_for_config_sync(
|
||||||
actions=client.db.litellm_proxymodeltable,
|
actions=client.db.litellm_proxymodeltable,
|
||||||
table_name="litellm_proxymodeltable",
|
table_name="litellm_proxymodeltable",
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
|
||||||
def _model_table(self) -> _ProxyModelActions:
|
|
||||||
return self.table
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def model_class(self) -> type[LiteLLM_ProxyModelTable]:
|
def model_class(self) -> type[LiteLLM_ProxyModelTable]:
|
||||||
return LiteLLM_ProxyModelTable
|
return LiteLLM_ProxyModelTable
|
||||||
|
|
@ -100,17 +92,17 @@ class ModelRepository(BaseRepository[LiteLLM_ProxyModelTable]):
|
||||||
|
|
||||||
async def find_by_name(self, model_name: str) -> list[LiteLLM_ProxyModelTable]:
|
async def find_by_name(self, model_name: str) -> list[LiteLLM_ProxyModelTable]:
|
||||||
"""Find models by name."""
|
"""Find models by name."""
|
||||||
records: Final = await self._model_table.find_many(where={"model_name": model_name})
|
records: Final = await self.table.find_many(where={"model_name": model_name})
|
||||||
return self._to_model_list(records)
|
return self._to_model_list(records)
|
||||||
|
|
||||||
async def find_all(self) -> list[LiteLLM_ProxyModelTable]:
|
async def find_all(self) -> list[LiteLLM_ProxyModelTable]:
|
||||||
"""Find all models."""
|
"""Find all models."""
|
||||||
records: Final = await self._model_table.find_many()
|
records: Final = await self.table.find_many()
|
||||||
return self._to_model_list(records)
|
return self._to_model_list(records)
|
||||||
|
|
||||||
async def find_unblocked(self) -> list[LiteLLM_ProxyModelTable]:
|
async def find_unblocked(self) -> list[LiteLLM_ProxyModelTable]:
|
||||||
"""Find all models that are not blocked."""
|
"""Find all models that are not blocked."""
|
||||||
records: Final = await self._model_table.find_many(where={"blocked": False})
|
records: Final = await self.table.find_many(where={"blocked": False})
|
||||||
return self._to_model_list(records)
|
return self._to_model_list(records)
|
||||||
|
|
||||||
async def find_by_team_id(self, team_id: str) -> list[LiteLLM_ProxyModelTable]:
|
async def find_by_team_id(self, team_id: str) -> list[LiteLLM_ProxyModelTable]:
|
||||||
|
|
@ -147,7 +139,7 @@ class ModelRepository(BaseRepository[LiteLLM_ProxyModelTable]):
|
||||||
if model_info is not None:
|
if model_info is not None:
|
||||||
data["model_info"] = json.dumps(model_info)
|
data["model_info"] = json.dumps(model_info)
|
||||||
|
|
||||||
record: Final = await self._model_table.create(data=data)
|
record: Final = await self.table.create(data=data)
|
||||||
model: Final = self._to_model(record)
|
model: Final = self._to_model(record)
|
||||||
assert model is not None
|
assert model is not None
|
||||||
return model
|
return model
|
||||||
|
|
@ -173,7 +165,7 @@ class ModelRepository(BaseRepository[LiteLLM_ProxyModelTable]):
|
||||||
if blocked is not None:
|
if blocked is not None:
|
||||||
data["blocked"] = blocked
|
data["blocked"] = blocked
|
||||||
|
|
||||||
record: Final = await self._model_table.update(where={"model_id": model_id}, data=data)
|
record: Final = await self.table.update(where={"model_id": model_id}, data=data)
|
||||||
return self._to_model(record)
|
return self._to_model(record)
|
||||||
|
|
||||||
async def delete_model(self, model_id: str) -> LiteLLM_ProxyModelTable | None:
|
async def delete_model(self, model_id: str) -> LiteLLM_ProxyModelTable | None:
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,21 @@
|
||||||
ObjectPermission repository for database operations on LiteLLM_ObjectPermissionTable.
|
ObjectPermission repository for database operations on LiteLLM_ObjectPermissionTable.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Any, Final
|
from typing import TYPE_CHECKING, Any, Final
|
||||||
|
|
||||||
from litellm.models.object_permission import LiteLLM_ObjectPermissionTable
|
from litellm.models.object_permission import LiteLLM_ObjectPermissionTable
|
||||||
from litellm.repositories.base_repository import BaseRepository
|
from litellm.repositories.base_repository import BaseRepository
|
||||||
|
from litellm.repositories.prisma_protocols import TableActions
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from prisma import models as prisma_models
|
||||||
|
|
||||||
|
|
||||||
class ObjectPermissionRepository(BaseRepository[LiteLLM_ObjectPermissionTable]):
|
class ObjectPermissionRepository(BaseRepository[LiteLLM_ObjectPermissionTable]):
|
||||||
"""Repository for object permission database operations."""
|
"""Repository for object permission database operations."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def table(self) -> Any:
|
def table(self) -> TableActions["prisma_models.LiteLLM_ObjectPermissionTable"]:
|
||||||
return self.prisma_client.db.litellm_objectpermissiontable
|
return self.prisma_client.db.litellm_objectpermissiontable
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,21 @@
|
||||||
Organization repository for database operations on LiteLLM_OrganizationTable.
|
Organization repository for database operations on LiteLLM_OrganizationTable.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Any, Final
|
from typing import TYPE_CHECKING, Any, Final
|
||||||
|
|
||||||
from litellm.models.organization import LiteLLM_OrganizationTable
|
from litellm.models.organization import LiteLLM_OrganizationTable
|
||||||
from litellm.repositories.base_repository import BaseRepository
|
from litellm.repositories.base_repository import BaseRepository
|
||||||
|
from litellm.repositories.prisma_protocols import TableActions
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from prisma import models as prisma_models
|
||||||
|
|
||||||
|
|
||||||
class OrganizationRepository(BaseRepository[LiteLLM_OrganizationTable]):
|
class OrganizationRepository(BaseRepository[LiteLLM_OrganizationTable]):
|
||||||
"""Repository for organization database operations."""
|
"""Repository for organization database operations."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def table(self) -> Any:
|
def table(self) -> TableActions["prisma_models.LiteLLM_OrganizationTable"]:
|
||||||
return self.prisma_client.db.litellm_organizationtable
|
return self.prisma_client.db.litellm_organizationtable
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,93 @@ from typing import Protocol, TypeVar
|
||||||
RowT_co = TypeVar("RowT_co", covariant=True)
|
RowT_co = TypeVar("RowT_co", covariant=True)
|
||||||
|
|
||||||
|
|
||||||
|
class TableActions(Protocol[RowT_co]):
|
||||||
|
"""The prisma-client-py per-model action surface, keyed to the row it returns.
|
||||||
|
|
||||||
|
Query inputs stay `Mapping[str, object]` rather than the generated
|
||||||
|
`types.*` TypedDicts so callers can keep passing plain dicts, while every
|
||||||
|
result carries the row type the repository is bound to.
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def find_unique(
|
||||||
|
self, where: Mapping[str, object], include: Mapping[str, object] | None = None
|
||||||
|
) -> RowT_co | None: ...
|
||||||
|
|
||||||
|
async def find_first(
|
||||||
|
self,
|
||||||
|
skip: int | None = None,
|
||||||
|
where: Mapping[str, object] | None = None,
|
||||||
|
cursor: Mapping[str, object] | None = None,
|
||||||
|
include: Mapping[str, object] | None = None,
|
||||||
|
order: Mapping[str, object] | Sequence[Mapping[str, object]] | None = None,
|
||||||
|
distinct: Sequence[str] | None = None,
|
||||||
|
) -> RowT_co | None: ...
|
||||||
|
|
||||||
|
async def find_many(
|
||||||
|
self,
|
||||||
|
take: int | None = None,
|
||||||
|
skip: int | None = None,
|
||||||
|
where: Mapping[str, object] | None = None,
|
||||||
|
cursor: Mapping[str, object] | None = None,
|
||||||
|
include: Mapping[str, object] | None = None,
|
||||||
|
order: Mapping[str, object] | Sequence[Mapping[str, object]] | None = None,
|
||||||
|
distinct: Sequence[str] | None = None,
|
||||||
|
) -> Sequence[RowT_co]: ...
|
||||||
|
|
||||||
|
async def create(self, data: Mapping[str, object], include: Mapping[str, object] | None = None) -> RowT_co: ...
|
||||||
|
|
||||||
|
async def create_many(
|
||||||
|
self, data: Sequence[Mapping[str, object]], *, skip_duplicates: bool | None = None
|
||||||
|
) -> int: ...
|
||||||
|
|
||||||
|
async def upsert(
|
||||||
|
self,
|
||||||
|
where: Mapping[str, object],
|
||||||
|
data: Mapping[str, object],
|
||||||
|
include: Mapping[str, object] | None = None,
|
||||||
|
) -> RowT_co: ...
|
||||||
|
|
||||||
|
async def update(
|
||||||
|
self,
|
||||||
|
data: Mapping[str, object],
|
||||||
|
where: Mapping[str, object],
|
||||||
|
include: Mapping[str, object] | None = None,
|
||||||
|
) -> RowT_co | None: ...
|
||||||
|
|
||||||
|
async def update_many(self, data: Mapping[str, object], where: Mapping[str, object]) -> int: ...
|
||||||
|
|
||||||
|
async def delete(
|
||||||
|
self, where: Mapping[str, object], include: Mapping[str, object] | None = None
|
||||||
|
) -> RowT_co | None: ...
|
||||||
|
|
||||||
|
async def delete_many(self, where: Mapping[str, object] | None = None) -> int: ...
|
||||||
|
|
||||||
|
async def count(
|
||||||
|
self,
|
||||||
|
select: None = None,
|
||||||
|
take: int | None = None,
|
||||||
|
skip: int | None = None,
|
||||||
|
where: Mapping[str, object] | None = None,
|
||||||
|
cursor: Mapping[str, object] | None = None,
|
||||||
|
) -> int: ...
|
||||||
|
|
||||||
|
async def group_by(
|
||||||
|
self,
|
||||||
|
by: Sequence[str],
|
||||||
|
*,
|
||||||
|
where: Mapping[str, object] | None = None,
|
||||||
|
take: int | None = None,
|
||||||
|
skip: int | None = None,
|
||||||
|
order: Mapping[str, object] | Sequence[Mapping[str, object]] | None = None,
|
||||||
|
having: Mapping[str, object] | None = None,
|
||||||
|
count: bool | Mapping[str, object] | None = None,
|
||||||
|
sum: bool | Mapping[str, object] | None = None,
|
||||||
|
avg: bool | Mapping[str, object] | None = None,
|
||||||
|
min: bool | Mapping[str, object] | None = None,
|
||||||
|
max: bool | Mapping[str, object] | None = None,
|
||||||
|
) -> Sequence[Mapping[str, object]]: ...
|
||||||
|
|
||||||
|
|
||||||
class PrismaRecord(Protocol):
|
class PrismaRecord(Protocol):
|
||||||
def dict(self) -> Mapping[str, object]: ...
|
def dict(self) -> Mapping[str, object]: ...
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,21 @@
|
||||||
Project repository for database operations on LiteLLM_ProjectTable.
|
Project repository for database operations on LiteLLM_ProjectTable.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Any, Final
|
from typing import TYPE_CHECKING, Any, Final
|
||||||
|
|
||||||
from litellm.models.project import LiteLLM_ProjectTable
|
from litellm.models.project import LiteLLM_ProjectTable
|
||||||
from litellm.repositories.base_repository import BaseRepository
|
from litellm.repositories.base_repository import BaseRepository
|
||||||
|
from litellm.repositories.prisma_protocols import TableActions
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from prisma import models as prisma_models
|
||||||
|
|
||||||
|
|
||||||
class ProjectRepository(BaseRepository[LiteLLM_ProjectTable]):
|
class ProjectRepository(BaseRepository[LiteLLM_ProjectTable]):
|
||||||
"""Repository for project database operations."""
|
"""Repository for project database operations."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def table(self) -> Any:
|
def table(self) -> TableActions["prisma_models.LiteLLM_ProjectTable"]:
|
||||||
return self.prisma_client.db.litellm_projecttable
|
return self.prisma_client.db.litellm_projecttable
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,16 @@ These are thin wrappers for tables that do not (yet) need domain-specific query
|
||||||
methods; richer repositories live in their own modules.
|
methods; richer repositories live in their own modules.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Any
|
from typing import TYPE_CHECKING, Any, Final, Generic
|
||||||
|
|
||||||
from litellm.proxy.common_utils.config_sync_pubsub import wrap_table_actions_for_config_sync
|
from litellm.proxy.common_utils.config_sync_pubsub import wrap_table_actions_for_config_sync
|
||||||
|
from litellm.repositories.prisma_protocols import RowT_co, TableActions
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from prisma import models as prisma_models # noqa: F401 # used by quoted base-class subscripts
|
||||||
|
|
||||||
|
|
||||||
class PrismaTableRepository:
|
class PrismaTableRepository(Generic[RowT_co]):
|
||||||
"""Base for repositories that expose a single Prisma table."""
|
"""Base for repositories that expose a single Prisma table."""
|
||||||
|
|
||||||
table_name: str
|
table_name: str
|
||||||
|
|
@ -27,208 +31,206 @@ class PrismaTableRepository:
|
||||||
return self._prisma_client
|
return self._prisma_client
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def table(self) -> Any: # any-ok: Prisma table actions are reached through the untyped client wrapper
|
def table(self) -> TableActions[RowT_co]:
|
||||||
return wrap_table_actions_for_config_sync(
|
actions: Final[TableActions[RowT_co]] = getattr(self.prisma_client.db, self.table_name)
|
||||||
actions=getattr(self.prisma_client.db, self.table_name),
|
return wrap_table_actions_for_config_sync(actions=actions, table_name=self.table_name)
|
||||||
table_name=self.table_name,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PolicyRepository(PrismaTableRepository):
|
class PolicyRepository(PrismaTableRepository["prisma_models.LiteLLM_PolicyTable"]):
|
||||||
table_name = "litellm_policytable"
|
table_name = "litellm_policytable"
|
||||||
|
|
||||||
|
|
||||||
class AgentsRepository(PrismaTableRepository):
|
class AgentsRepository(PrismaTableRepository["prisma_models.LiteLLM_AgentsTable"]):
|
||||||
table_name = "litellm_agentstable"
|
table_name = "litellm_agentstable"
|
||||||
|
|
||||||
|
|
||||||
class ObjectPermissionRepository(PrismaTableRepository):
|
class ObjectPermissionRepository(PrismaTableRepository["prisma_models.LiteLLM_ObjectPermissionTable"]):
|
||||||
table_name = "litellm_objectpermissiontable"
|
table_name = "litellm_objectpermissiontable"
|
||||||
|
|
||||||
|
|
||||||
class GuardrailsRepository(PrismaTableRepository):
|
class GuardrailsRepository(PrismaTableRepository["prisma_models.LiteLLM_GuardrailsTable"]):
|
||||||
table_name = "litellm_guardrailstable"
|
table_name = "litellm_guardrailstable"
|
||||||
|
|
||||||
|
|
||||||
class MCPServerRepository(PrismaTableRepository):
|
class MCPServerRepository(PrismaTableRepository["prisma_models.LiteLLM_MCPServerTable"]):
|
||||||
table_name = "litellm_mcpservertable"
|
table_name = "litellm_mcpservertable"
|
||||||
|
|
||||||
|
|
||||||
class ManagedObjectRepository(PrismaTableRepository):
|
class ManagedObjectRepository(PrismaTableRepository["prisma_models.LiteLLM_ManagedObjectTable"]):
|
||||||
table_name = "litellm_managedobjecttable"
|
table_name = "litellm_managedobjecttable"
|
||||||
|
|
||||||
|
|
||||||
class OrganizationMembershipRepository(PrismaTableRepository):
|
class OrganizationMembershipRepository(PrismaTableRepository["prisma_models.LiteLLM_OrganizationMembership"]):
|
||||||
table_name = "litellm_organizationmembership"
|
table_name = "litellm_organizationmembership"
|
||||||
|
|
||||||
|
|
||||||
class SpendLogsRepository(PrismaTableRepository):
|
class SpendLogsRepository(PrismaTableRepository["prisma_models.LiteLLM_SpendLogs"]):
|
||||||
table_name = "litellm_spendlogs"
|
table_name = "litellm_spendlogs"
|
||||||
|
|
||||||
|
|
||||||
class ClaudeCodePluginRepository(PrismaTableRepository):
|
class ClaudeCodePluginRepository(PrismaTableRepository["prisma_models.LiteLLM_ClaudeCodePluginTable"]):
|
||||||
table_name = "litellm_claudecodeplugintable"
|
table_name = "litellm_claudecodeplugintable"
|
||||||
|
|
||||||
|
|
||||||
class TeamMembershipRepository(PrismaTableRepository):
|
class TeamMembershipRepository(PrismaTableRepository["prisma_models.LiteLLM_TeamMembership"]):
|
||||||
table_name = "litellm_teammembership"
|
table_name = "litellm_teammembership"
|
||||||
|
|
||||||
|
|
||||||
class EndUserRepository(PrismaTableRepository):
|
class EndUserRepository(PrismaTableRepository["prisma_models.LiteLLM_EndUserTable"]):
|
||||||
table_name = "litellm_endusertable"
|
table_name = "litellm_endusertable"
|
||||||
|
|
||||||
|
|
||||||
class ManagedVectorStoresRepository(PrismaTableRepository):
|
class ManagedVectorStoresRepository(PrismaTableRepository["prisma_models.LiteLLM_ManagedVectorStoresTable"]):
|
||||||
table_name = "litellm_managedvectorstorestable"
|
table_name = "litellm_managedvectorstorestable"
|
||||||
|
|
||||||
|
|
||||||
class MCPUserCredentialsRepository(PrismaTableRepository):
|
class MCPUserCredentialsRepository(PrismaTableRepository["prisma_models.LiteLLM_MCPUserCredentials"]):
|
||||||
table_name = "litellm_mcpusercredentials"
|
table_name = "litellm_mcpusercredentials"
|
||||||
|
|
||||||
|
|
||||||
class MCPServerOAuthClientRepository(PrismaTableRepository):
|
class MCPServerOAuthClientRepository(PrismaTableRepository["prisma_models.LiteLLM_MCPServerOAuthClient"]):
|
||||||
table_name = "litellm_mcpserveroauthclient"
|
table_name = "litellm_mcpserveroauthclient"
|
||||||
|
|
||||||
|
|
||||||
class PromptRepository(PrismaTableRepository):
|
class PromptRepository(PrismaTableRepository["prisma_models.LiteLLM_PromptTable"]):
|
||||||
table_name = "litellm_prompttable"
|
table_name = "litellm_prompttable"
|
||||||
|
|
||||||
|
|
||||||
class TagRepository(PrismaTableRepository):
|
class TagRepository(PrismaTableRepository["prisma_models.LiteLLM_TagTable"]):
|
||||||
table_name = "litellm_tagtable"
|
table_name = "litellm_tagtable"
|
||||||
|
|
||||||
|
|
||||||
class InvitationLinkRepository(PrismaTableRepository):
|
class InvitationLinkRepository(PrismaTableRepository["prisma_models.LiteLLM_InvitationLink"]):
|
||||||
table_name = "litellm_invitationlink"
|
table_name = "litellm_invitationlink"
|
||||||
|
|
||||||
|
|
||||||
class JWTKeyMappingRepository(PrismaTableRepository):
|
class JWTKeyMappingRepository(PrismaTableRepository["prisma_models.LiteLLM_JWTKeyMapping"]):
|
||||||
table_name = "litellm_jwtkeymapping"
|
table_name = "litellm_jwtkeymapping"
|
||||||
|
|
||||||
|
|
||||||
class ManagedFileRepository(PrismaTableRepository):
|
class ManagedFileRepository(PrismaTableRepository["prisma_models.LiteLLM_ManagedFileTable"]):
|
||||||
table_name = "litellm_managedfiletable"
|
table_name = "litellm_managedfiletable"
|
||||||
|
|
||||||
|
|
||||||
class MemoryRepository(PrismaTableRepository):
|
class MemoryRepository(PrismaTableRepository["prisma_models.LiteLLM_MemoryTable"]):
|
||||||
table_name = "litellm_memorytable"
|
table_name = "litellm_memorytable"
|
||||||
|
|
||||||
|
|
||||||
class SearchToolsRepository(PrismaTableRepository):
|
class SearchToolsRepository(PrismaTableRepository["prisma_models.LiteLLM_SearchToolsTable"]):
|
||||||
table_name = "litellm_searchtoolstable"
|
table_name = "litellm_searchtoolstable"
|
||||||
|
|
||||||
|
|
||||||
class ConfigOverridesRepository(PrismaTableRepository):
|
class ConfigOverridesRepository(PrismaTableRepository["prisma_models.LiteLLM_ConfigOverrides"]):
|
||||||
table_name = "litellm_configoverrides"
|
table_name = "litellm_configoverrides"
|
||||||
|
|
||||||
|
|
||||||
class MCPToolsetRepository(PrismaTableRepository):
|
class MCPToolsetRepository(PrismaTableRepository["prisma_models.LiteLLM_MCPToolsetTable"]):
|
||||||
table_name = "litellm_mcptoolsettable"
|
table_name = "litellm_mcptoolsettable"
|
||||||
|
|
||||||
|
|
||||||
class ToolRepository(PrismaTableRepository):
|
class ToolRepository(PrismaTableRepository["prisma_models.LiteLLM_ToolTable"]):
|
||||||
table_name = "litellm_tooltable"
|
table_name = "litellm_tooltable"
|
||||||
|
|
||||||
|
|
||||||
class DeletedVerificationTokenRepository(PrismaTableRepository):
|
class DeletedVerificationTokenRepository(PrismaTableRepository["prisma_models.LiteLLM_DeletedVerificationToken"]):
|
||||||
table_name = "litellm_deletedverificationtoken"
|
table_name = "litellm_deletedverificationtoken"
|
||||||
|
|
||||||
|
|
||||||
class WorkflowRunRepository(PrismaTableRepository):
|
class WorkflowRunRepository(PrismaTableRepository["prisma_models.LiteLLM_WorkflowRun"]):
|
||||||
table_name = "litellm_workflowrun"
|
table_name = "litellm_workflowrun"
|
||||||
|
|
||||||
|
|
||||||
class ModelTableRepository(PrismaTableRepository):
|
class ModelTableRepository(PrismaTableRepository["prisma_models.LiteLLM_ModelTable"]):
|
||||||
table_name = "litellm_modeltable"
|
table_name = "litellm_modeltable"
|
||||||
|
|
||||||
|
|
||||||
class AccessGroupRepository(PrismaTableRepository):
|
class AccessGroupRepository(PrismaTableRepository["prisma_models.LiteLLM_AccessGroupTable"]):
|
||||||
table_name = "litellm_accessgrouptable"
|
table_name = "litellm_accessgrouptable"
|
||||||
|
|
||||||
|
|
||||||
class SSOConfigRepository(PrismaTableRepository):
|
class SSOConfigRepository(PrismaTableRepository["prisma_models.LiteLLM_SSOConfig"]):
|
||||||
table_name = "litellm_ssoconfig"
|
table_name = "litellm_ssoconfig"
|
||||||
|
|
||||||
|
|
||||||
class UISettingsRepository(PrismaTableRepository):
|
class UISettingsRepository(PrismaTableRepository["prisma_models.LiteLLM_UISettings"]):
|
||||||
table_name = "litellm_uisettings"
|
table_name = "litellm_uisettings"
|
||||||
|
|
||||||
|
|
||||||
class DailyGuardrailMetricsRepository(PrismaTableRepository):
|
class DailyGuardrailMetricsRepository(PrismaTableRepository["prisma_models.LiteLLM_DailyGuardrailMetrics"]):
|
||||||
table_name = "litellm_dailyguardrailmetrics"
|
table_name = "litellm_dailyguardrailmetrics"
|
||||||
|
|
||||||
|
|
||||||
class DailyGuardrailUsageUnitsRepository(PrismaTableRepository):
|
class DailyGuardrailUsageUnitsRepository(PrismaTableRepository["prisma_models.LiteLLM_DailyGuardrailUsageUnits"]):
|
||||||
table_name = "litellm_dailyguardrailusageunits"
|
table_name = "litellm_dailyguardrailusageunits"
|
||||||
|
|
||||||
|
|
||||||
class PolicyAttachmentRepository(PrismaTableRepository):
|
class PolicyAttachmentRepository(PrismaTableRepository["prisma_models.LiteLLM_PolicyAttachmentTable"]):
|
||||||
table_name = "litellm_policyattachmenttable"
|
table_name = "litellm_policyattachmenttable"
|
||||||
|
|
||||||
|
|
||||||
class DeletedTeamRepository(PrismaTableRepository):
|
class DeletedTeamRepository(PrismaTableRepository["prisma_models.LiteLLM_DeletedTeamTable"]):
|
||||||
table_name = "litellm_deletedteamtable"
|
table_name = "litellm_deletedteamtable"
|
||||||
|
|
||||||
|
|
||||||
class SkillsRepository(PrismaTableRepository):
|
class SkillsRepository(PrismaTableRepository["prisma_models.LiteLLM_SkillsTable"]):
|
||||||
table_name = "litellm_skillstable"
|
table_name = "litellm_skillstable"
|
||||||
|
|
||||||
|
|
||||||
class CacheConfigRepository(PrismaTableRepository):
|
class CacheConfigRepository(PrismaTableRepository["prisma_models.LiteLLM_CacheConfig"]):
|
||||||
table_name = "litellm_cacheconfig"
|
table_name = "litellm_cacheconfig"
|
||||||
|
|
||||||
|
|
||||||
class ManagedVectorStoreIndexRepository(PrismaTableRepository):
|
class ManagedVectorStoreIndexRepository(PrismaTableRepository["prisma_models.LiteLLM_ManagedVectorStoreIndexTable"]):
|
||||||
table_name = "litellm_managedvectorstoreindextable"
|
table_name = "litellm_managedvectorstoreindextable"
|
||||||
|
|
||||||
|
|
||||||
class WorkflowMessageRepository(PrismaTableRepository):
|
class WorkflowMessageRepository(PrismaTableRepository["prisma_models.LiteLLM_WorkflowMessage"]):
|
||||||
table_name = "litellm_workflowmessage"
|
table_name = "litellm_workflowmessage"
|
||||||
|
|
||||||
|
|
||||||
class DailyTagSpendRepository(PrismaTableRepository):
|
class DailyTagSpendRepository(PrismaTableRepository["prisma_models.LiteLLM_DailyTagSpend"]):
|
||||||
table_name = "litellm_dailytagspend"
|
table_name = "litellm_dailytagspend"
|
||||||
|
|
||||||
|
|
||||||
class SpendLogToolIndexRepository(PrismaTableRepository):
|
class SpendLogToolIndexRepository(PrismaTableRepository["prisma_models.LiteLLM_SpendLogToolIndex"]):
|
||||||
table_name = "litellm_spendlogtoolindex"
|
table_name = "litellm_spendlogtoolindex"
|
||||||
|
|
||||||
|
|
||||||
class DailyToolSpendRepository(PrismaTableRepository):
|
class DailyToolSpendRepository(PrismaTableRepository["prisma_models.LiteLLM_DailyToolSpend"]):
|
||||||
table_name = "litellm_dailytoolspend"
|
table_name = "litellm_dailytoolspend"
|
||||||
|
|
||||||
|
|
||||||
class SpendLogGuardrailIndexRepository(PrismaTableRepository):
|
class SpendLogGuardrailIndexRepository(PrismaTableRepository["prisma_models.LiteLLM_SpendLogGuardrailIndex"]):
|
||||||
table_name = "litellm_spendlogguardrailindex"
|
table_name = "litellm_spendlogguardrailindex"
|
||||||
|
|
||||||
|
|
||||||
class UserNotificationsRepository(PrismaTableRepository):
|
class UserNotificationsRepository(PrismaTableRepository["prisma_models.LiteLLM_UserNotifications"]):
|
||||||
table_name = "litellm_usernotifications"
|
table_name = "litellm_usernotifications"
|
||||||
|
|
||||||
|
|
||||||
class HealthCheckRepository(PrismaTableRepository):
|
class HealthCheckRepository(PrismaTableRepository["prisma_models.LiteLLM_HealthCheckTable"]):
|
||||||
table_name = "litellm_healthchecktable"
|
table_name = "litellm_healthchecktable"
|
||||||
|
|
||||||
|
|
||||||
class DeprecatedVerificationTokenRepository(PrismaTableRepository):
|
class DeprecatedVerificationTokenRepository(PrismaTableRepository["prisma_models.LiteLLM_DeprecatedVerificationToken"]):
|
||||||
table_name = "litellm_deprecatedverificationtoken"
|
table_name = "litellm_deprecatedverificationtoken"
|
||||||
|
|
||||||
|
|
||||||
class WorkflowEventRepository(PrismaTableRepository):
|
class WorkflowEventRepository(PrismaTableRepository["prisma_models.LiteLLM_WorkflowEvent"]):
|
||||||
table_name = "litellm_workflowevent"
|
table_name = "litellm_workflowevent"
|
||||||
|
|
||||||
|
|
||||||
class DailyPolicyMetricsRepository(PrismaTableRepository):
|
class DailyPolicyMetricsRepository(PrismaTableRepository["prisma_models.LiteLLM_DailyPolicyMetrics"]):
|
||||||
table_name = "litellm_dailypolicymetrics"
|
table_name = "litellm_dailypolicymetrics"
|
||||||
|
|
||||||
|
|
||||||
class AdaptiveRouterStateRepository(PrismaTableRepository):
|
class AdaptiveRouterStateRepository(PrismaTableRepository["prisma_models.LiteLLM_AdaptiveRouterState"]):
|
||||||
table_name = "litellm_adaptiverouterstate"
|
table_name = "litellm_adaptiverouterstate"
|
||||||
|
|
||||||
|
|
||||||
class AuditLogRepository(PrismaTableRepository):
|
class AuditLogRepository(PrismaTableRepository["prisma_models.LiteLLM_AuditLog"]):
|
||||||
table_name = "litellm_auditlog"
|
table_name = "litellm_auditlog"
|
||||||
|
|
||||||
|
|
||||||
class AdaptiveRouterSessionRepository(PrismaTableRepository):
|
class AdaptiveRouterSessionRepository(PrismaTableRepository["prisma_models.LiteLLM_AdaptiveRouterSession"]):
|
||||||
table_name = "litellm_adaptiveroutersession"
|
table_name = "litellm_adaptiveroutersession"
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ Team repository for database operations on LiteLLM_TeamTable.
|
||||||
import json
|
import json
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import TYPE_CHECKING, Any, Final
|
from typing import TYPE_CHECKING, Final
|
||||||
|
|
||||||
from pydantic import TypeAdapter
|
from pydantic import TypeAdapter
|
||||||
|
|
||||||
|
|
@ -15,9 +15,11 @@ from litellm.repositories.base_repository import (
|
||||||
DbRecord,
|
DbRecord,
|
||||||
record_to_dict,
|
record_to_dict,
|
||||||
)
|
)
|
||||||
|
from litellm.repositories.prisma_protocols import TableActions
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from prisma import Prisma
|
from prisma import Prisma
|
||||||
|
from prisma import models as prisma_models
|
||||||
|
|
||||||
_MEMBERS_WITH_ROLES_ADAPTER: Final = TypeAdapter(list[Member])
|
_MEMBERS_WITH_ROLES_ADAPTER: Final = TypeAdapter(list[Member])
|
||||||
_JSON_ENCODED_TEAM_FIELDS: Final = (
|
_JSON_ENCODED_TEAM_FIELDS: Final = (
|
||||||
|
|
@ -34,11 +36,11 @@ class TeamRepository(BaseRepository[LiteLLM_TeamTable]):
|
||||||
"""Repository for team database operations."""
|
"""Repository for team database operations."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def table(self) -> Any: # any-ok: PrismaClient.db is an untyped runtime wrapper
|
def table(self) -> TableActions["prisma_models.LiteLLM_TeamTable"]:
|
||||||
return self.prisma_client.db.litellm_teamtable
|
return self.prisma_client.db.litellm_teamtable
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def deleted_table(self) -> Any: # any-ok: PrismaClient.db is an untyped runtime wrapper
|
def deleted_table(self) -> TableActions["prisma_models.LiteLLM_DeletedTeamTable"]:
|
||||||
return self.prisma_client.db.litellm_deletedteamtable
|
return self.prisma_client.db.litellm_deletedteamtable
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
from typing import Final
|
from typing import TYPE_CHECKING, Final
|
||||||
|
|
||||||
from litellm.repositories.table_repositories import PrismaTableRepository
|
from litellm.repositories.table_repositories import PrismaTableRepository
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from prisma import models as prisma_models # noqa: F401 # resolved only from the quoted base-class subscript below
|
||||||
|
|
||||||
USER_BANNER_ROW_ID: Final = "user_banner"
|
USER_BANNER_ROW_ID: Final = "user_banner"
|
||||||
|
|
||||||
|
|
||||||
class UserBannerRepository(PrismaTableRepository):
|
class UserBannerRepository(PrismaTableRepository["prisma_models.LiteLLM_UISettings"]):
|
||||||
table_name = "litellm_uisettings"
|
table_name = "litellm_uisettings"
|
||||||
|
|
||||||
async def get_raw_settings(self) -> object:
|
async def get_raw_settings(self) -> object:
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,14 @@ User repository for database operations on LiteLLM_UserTable.
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from typing import Any, Final
|
from typing import TYPE_CHECKING, Final
|
||||||
|
|
||||||
from litellm.models.user import LiteLLM_UserTable
|
from litellm.models.user import LiteLLM_UserTable
|
||||||
from litellm.repositories.base_repository import BaseRepository, DbRecord, record_to_dict
|
from litellm.repositories.base_repository import BaseRepository, DbRecord, record_to_dict
|
||||||
|
from litellm.repositories.prisma_protocols import TableActions
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from prisma import models as prisma_models
|
||||||
|
|
||||||
_JSON_ENCODED_COLUMNS: Final = frozenset({"metadata", "model_spend", "model_max_budget"})
|
_JSON_ENCODED_COLUMNS: Final = frozenset({"metadata", "model_spend", "model_max_budget"})
|
||||||
|
|
||||||
|
|
@ -16,7 +20,7 @@ class UserRepository(BaseRepository[LiteLLM_UserTable]):
|
||||||
"""Repository for user database operations."""
|
"""Repository for user database operations."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def table(self) -> Any: # any-ok: Prisma table actions are reached through the untyped client wrapper
|
def table(self) -> TableActions["prisma_models.LiteLLM_UserTable"]:
|
||||||
return self.prisma_client.db.litellm_usertable
|
return self.prisma_client.db.litellm_usertable
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@ VerificationToken repository for database operations on LiteLLM_VerificationToke
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping, Sequence
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import TYPE_CHECKING, Any, Final
|
from typing import TYPE_CHECKING, Final
|
||||||
|
|
||||||
from litellm.models.verification_token import (
|
from litellm.models.verification_token import (
|
||||||
LiteLLM_VerificationToken,
|
LiteLLM_VerificationToken,
|
||||||
|
|
@ -15,8 +15,12 @@ from litellm.repositories.base_repository import (
|
||||||
DbRecord,
|
DbRecord,
|
||||||
record_to_dict,
|
record_to_dict,
|
||||||
)
|
)
|
||||||
|
from litellm.repositories.prisma_protocols import TableActions
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from prisma.models import (
|
||||||
|
LiteLLM_DeletedVerificationToken as PrismaDeletedVerificationToken,
|
||||||
|
)
|
||||||
from prisma.models import (
|
from prisma.models import (
|
||||||
LiteLLM_VerificationToken as PrismaVerificationToken,
|
LiteLLM_VerificationToken as PrismaVerificationToken,
|
||||||
)
|
)
|
||||||
|
|
@ -45,11 +49,11 @@ class VerificationTokenRepository(BaseRepository[LiteLLM_VerificationToken]):
|
||||||
return prisma_client
|
return prisma_client
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def table(self) -> Any:
|
def table(self) -> TableActions["PrismaVerificationToken"]:
|
||||||
return self.prisma_client.db.litellm_verificationtoken
|
return self.prisma_client.db.litellm_verificationtoken
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def deleted_table(self) -> Any:
|
def deleted_table(self) -> TableActions["PrismaDeletedVerificationToken"]:
|
||||||
return self.prisma_client.db.litellm_deletedverificationtoken
|
return self.prisma_client.db.litellm_deletedverificationtoken
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
@ -79,29 +83,29 @@ class VerificationTokenRepository(BaseRepository[LiteLLM_VerificationToken]):
|
||||||
|
|
||||||
async def find_by_alias(self, key_alias: str) -> LiteLLM_VerificationToken | None:
|
async def find_by_alias(self, key_alias: str) -> LiteLLM_VerificationToken | None:
|
||||||
"""Find a token by key alias."""
|
"""Find a token by key alias."""
|
||||||
records: Final[list[PrismaVerificationToken]] = await self.table.find_many(where={"key_alias": key_alias})
|
records: Final[Sequence[PrismaVerificationToken]] = await self.table.find_many(where={"key_alias": key_alias})
|
||||||
if records:
|
if records:
|
||||||
return self._to_model(records[0])
|
return self._to_model(records[0])
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def find_by_user_id(self, user_id: str) -> list[LiteLLM_VerificationToken]:
|
async def find_by_user_id(self, user_id: str) -> list[LiteLLM_VerificationToken]:
|
||||||
"""Find all tokens belonging to a user."""
|
"""Find all tokens belonging to a user."""
|
||||||
records: Final[list[PrismaVerificationToken]] = await self.table.find_many(where={"user_id": user_id})
|
records: Final[Sequence[PrismaVerificationToken]] = await self.table.find_many(where={"user_id": user_id})
|
||||||
return self._to_model_list(records)
|
return self._to_model_list(records)
|
||||||
|
|
||||||
async def find_by_team_id(self, team_id: str) -> list[LiteLLM_VerificationToken]:
|
async def find_by_team_id(self, team_id: str) -> list[LiteLLM_VerificationToken]:
|
||||||
"""Find all tokens belonging to a team."""
|
"""Find all tokens belonging to a team."""
|
||||||
records: Final[list[PrismaVerificationToken]] = await self.table.find_many(where={"team_id": team_id})
|
records: Final[Sequence[PrismaVerificationToken]] = await self.table.find_many(where={"team_id": team_id})
|
||||||
return self._to_model_list(records)
|
return self._to_model_list(records)
|
||||||
|
|
||||||
async def find_by_project_id(self, project_id: str) -> list[LiteLLM_VerificationToken]:
|
async def find_by_project_id(self, project_id: str) -> list[LiteLLM_VerificationToken]:
|
||||||
"""Find all tokens belonging to a project."""
|
"""Find all tokens belonging to a project."""
|
||||||
records: Final[list[PrismaVerificationToken]] = await self.table.find_many(where={"project_id": project_id})
|
records: Final[Sequence[PrismaVerificationToken]] = await self.table.find_many(where={"project_id": project_id})
|
||||||
return self._to_model_list(records)
|
return self._to_model_list(records)
|
||||||
|
|
||||||
async def find_active_tokens(self) -> list[LiteLLM_VerificationToken]:
|
async def find_active_tokens(self) -> list[LiteLLM_VerificationToken]:
|
||||||
"""Find all active (non-expired, non-blocked) tokens."""
|
"""Find all active (non-expired, non-blocked) tokens."""
|
||||||
records: Final[list[PrismaVerificationToken]] = await self.table.find_many(
|
records: Final[Sequence[PrismaVerificationToken]] = await self.table.find_many(
|
||||||
where={
|
where={
|
||||||
"blocked": {"not": True},
|
"blocked": {"not": True},
|
||||||
"OR": [{"expires": None}, {"expires": {"gt": datetime.utcnow()}}],
|
"OR": [{"expires": None}, {"expires": {"gt": datetime.utcnow()}}],
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,9 @@ import json
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
from collections.abc import Iterable, Sequence
|
from collections.abc import Iterable, Sequence
|
||||||
from typing import TYPE_CHECKING, Any, Final, TypeAlias, cast
|
from typing import TYPE_CHECKING, Any, Final, TypeAlias, cast # noqa: TID251 # see kwargs-ok / cast-ok markers
|
||||||
|
|
||||||
|
from typing_extensions import NotRequired, ReadOnly, TypedDict
|
||||||
|
|
||||||
from litellm._internal_context import is_internal_call
|
from litellm._internal_context import is_internal_call
|
||||||
from litellm._logging import verbose_logger
|
from litellm._logging import verbose_logger
|
||||||
|
|
@ -31,6 +33,12 @@ ToolParam: TypeAlias = object
|
||||||
FILE_SEARCH_FUNCTION_NAME: Final = "litellm_file_search"
|
FILE_SEARCH_FUNCTION_NAME: Final = "litellm_file_search"
|
||||||
|
|
||||||
|
|
||||||
|
class FileSearchToolCallArgs(TypedDict):
|
||||||
|
queries: ReadOnly[NotRequired[object]]
|
||||||
|
query: ReadOnly[NotRequired[object]]
|
||||||
|
vector_store_id: ReadOnly[NotRequired[object]]
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Detection
|
# Detection
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -175,13 +183,20 @@ async def _run_vector_searches(
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def _get_field(result: object, key: str, default: object = None) -> Any:
|
def _get_field(result: object, key: str, default: object = None) -> object:
|
||||||
"""Read a field from either a dict/TypedDict or an attribute-based object."""
|
"""Read a field from either a dict/TypedDict or an attribute-based object."""
|
||||||
if isinstance(result, dict):
|
if isinstance(result, dict):
|
||||||
return result.get(key, default)
|
return result.get(key, default)
|
||||||
return getattr(result, key, default)
|
return getattr(result, key, default)
|
||||||
|
|
||||||
|
|
||||||
|
def _joined_content_text(result: object) -> str:
|
||||||
|
"""Concatenate the text of every content chunk on a search result."""
|
||||||
|
content_items: Final = cast(Iterable[object], _get_field(result, "content") or []) # cast-ok: iterated as today
|
||||||
|
text_chunks: Final = [c.get("text", "") if isinstance(c, dict) else getattr(c, "text", "") for c in content_items]
|
||||||
|
return " ".join(t for t in text_chunks if t)
|
||||||
|
|
||||||
|
|
||||||
def _format_search_results_as_tool_output(
|
def _format_search_results_as_tool_output(
|
||||||
results: list[VectorStoreSearchResult],
|
results: list[VectorStoreSearchResult],
|
||||||
) -> str:
|
) -> str:
|
||||||
|
|
@ -194,9 +209,7 @@ def _format_search_results_as_tool_output(
|
||||||
score = _get_field(result, "score")
|
score = _get_field(result, "score")
|
||||||
file_id = _get_field(result, "file_id")
|
file_id = _get_field(result, "file_id")
|
||||||
filename = _get_field(result, "filename")
|
filename = _get_field(result, "filename")
|
||||||
content_items = _get_field(result, "content") or []
|
text = _joined_content_text(result)
|
||||||
text_chunks = [c.get("text", "") if isinstance(c, dict) else getattr(c, "text", "") for c in content_items]
|
|
||||||
text = " ".join(t for t in text_chunks if t)
|
|
||||||
|
|
||||||
header = f"[Result {i}"
|
header = f"[Result {i}"
|
||||||
if filename:
|
if filename:
|
||||||
|
|
@ -226,9 +239,7 @@ def _build_search_results_for_include(
|
||||||
formatted: Final[list[dict[str, object]]] = []
|
formatted: Final[list[dict[str, object]]] = []
|
||||||
for result in results:
|
for result in results:
|
||||||
file_id = _get_field(result, "file_id") or ""
|
file_id = _get_field(result, "file_id") or ""
|
||||||
content_items = _get_field(result, "content") or []
|
text = _joined_content_text(result)
|
||||||
text_chunks = [c.get("text", "") if isinstance(c, dict) else getattr(c, "text", "") for c in content_items]
|
|
||||||
text = " ".join(t for t in text_chunks if t)
|
|
||||||
formatted.append(
|
formatted.append(
|
||||||
{
|
{
|
||||||
"file_id": file_id,
|
"file_id": file_id,
|
||||||
|
|
@ -353,14 +364,14 @@ def _synthesize_responses_api_response(
|
||||||
created_at=getattr(original_response, "created_at", int(time.time())),
|
created_at=getattr(original_response, "created_at", int(time.time())),
|
||||||
status="completed",
|
status="completed",
|
||||||
model=getattr(original_response, "model", ""),
|
model=getattr(original_response, "model", ""),
|
||||||
output=cast(list[ResponseOutputItem | dict[str, Any]], synthesized_output),
|
output=cast(list[ResponseOutputItem | dict[str, object]], synthesized_output), # cast-ok: list is invariant
|
||||||
usage=getattr(original_response, "usage", None),
|
usage=getattr(original_response, "usage", None),
|
||||||
error=None,
|
error=None,
|
||||||
)
|
)
|
||||||
if hasattr(original_response, "_hidden_params"):
|
if hasattr(original_response, "_hidden_params"):
|
||||||
hidden: Final = dict(getattr(original_response, "_hidden_params") or {})
|
hidden: Final = dict(getattr(original_response, "_hidden_params") or {})
|
||||||
if first_response is not None and hasattr(first_response, "_hidden_params"):
|
if first_response is not None and hasattr(first_response, "_hidden_params"):
|
||||||
first_hidden: Final = getattr(first_response, "_hidden_params") or {}
|
first_hidden: Final[object] = getattr(first_response, "_hidden_params") or {}
|
||||||
first_cost: Final = (
|
first_cost: Final = (
|
||||||
first_hidden.get("response_cost")
|
first_hidden.get("response_cost")
|
||||||
if isinstance(first_hidden, dict)
|
if isinstance(first_hidden, dict)
|
||||||
|
|
@ -385,9 +396,10 @@ async def _call_aresponses(input, model, tools, **kwargs): # pragma: no cover
|
||||||
|
|
||||||
|
|
||||||
def _prepare_emulated_file_search_call(
|
def _prepare_emulated_file_search_call(
|
||||||
kwargs: dict[str, Any],
|
kwargs: dict[str, object],
|
||||||
) -> tuple[bool, dict[str, object]]:
|
) -> tuple[bool, dict[str, object]]:
|
||||||
include_items: Final[list[str]] = list(kwargs.get("include") or [])
|
raw_include: Final = kwargs.get("include") or []
|
||||||
|
include_items: Final[list[object]] = list(cast(Iterable[object], raw_include)) # cast-ok: iterated as today
|
||||||
include_search_results: Final = "file_search_call.results" in include_items
|
include_search_results: Final = "file_search_call.results" in include_items
|
||||||
|
|
||||||
original_stream: Final = kwargs.get("stream")
|
original_stream: Final = kwargs.get("stream")
|
||||||
|
|
@ -413,16 +425,16 @@ def _extract_tool_call_fields(tool_call: object, fallback_call_id: str) -> tuple
|
||||||
return call_id, raw_args
|
return call_id, raw_args
|
||||||
|
|
||||||
|
|
||||||
def _resolve_queries_from_args(args: dict[str, Any], input: object) -> list[str]:
|
def _resolve_queries_from_args(args: FileSearchToolCallArgs, input: object) -> list[str]:
|
||||||
"""Pull the queries list out of parsed tool-call arguments, with backward-compat fallbacks."""
|
"""Pull the queries list out of parsed tool-call arguments, with backward-compat fallbacks."""
|
||||||
queries_from_call: Final = args.get("queries")
|
queries_from_call: Final = args.get("queries")
|
||||||
if not queries_from_call:
|
if not queries_from_call:
|
||||||
# Fallback: check for single "query" field (backward compat)
|
# Fallback: check for single "query" field (backward compat)
|
||||||
single_query: Final = args.get("query")
|
single_query: Final = args.get("query")
|
||||||
return [single_query] if single_query else [str(input)]
|
return [cast(str, single_query)] if single_query else [str(input)] # cast-ok: model-supplied, as today
|
||||||
if not isinstance(queries_from_call, list):
|
if not isinstance(queries_from_call, list):
|
||||||
return [str(queries_from_call)]
|
return [str(queries_from_call)]
|
||||||
return queries_from_call
|
return cast(list[str], queries_from_call) # cast-ok: model-supplied elements, forwarded unchecked as today
|
||||||
|
|
||||||
|
|
||||||
async def _execute_file_search_tool_calls(
|
async def _execute_file_search_tool_calls(
|
||||||
|
|
@ -440,14 +452,14 @@ async def _execute_file_search_tool_calls(
|
||||||
call_id, raw_args = _extract_tool_call_fields(tool_call, fallback_call_id=file_search_call_id)
|
call_id, raw_args = _extract_tool_call_fields(tool_call, fallback_call_id=file_search_call_id)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
args = json.loads(raw_args) if isinstance(raw_args, str) else raw_args
|
args: FileSearchToolCallArgs = json.loads(raw_args) if isinstance(raw_args, str) else raw_args
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
args = {}
|
args = {}
|
||||||
|
|
||||||
queries_from_call = _resolve_queries_from_args(args, input)
|
queries_from_call = _resolve_queries_from_args(args, input)
|
||||||
|
|
||||||
vs_id_arg = args.get("vector_store_id")
|
vs_id_arg = args.get("vector_store_id")
|
||||||
vs_ids_for_call = [vs_id_arg] if vs_id_arg else all_vs_ids
|
vs_ids_for_call = [cast(str, vs_id_arg)] if vs_id_arg else all_vs_ids # cast-ok: model-supplied, as today
|
||||||
|
|
||||||
queries, results = await _run_vector_searches(
|
queries, results = await _run_vector_searches(
|
||||||
queries=queries_from_call,
|
queries=queries_from_call,
|
||||||
|
|
@ -481,7 +493,7 @@ def _build_follow_up_input(
|
||||||
original_input_items: Final[list[object]] = (
|
original_input_items: Final[list[object]] = (
|
||||||
list(input) if isinstance(input, (list, tuple)) else [{"role": "user", "content": str(input)}]
|
list(input) if isinstance(input, (list, tuple)) else [{"role": "user", "content": str(input)}]
|
||||||
)
|
)
|
||||||
first_response_output_items: Final[list[Any]] = []
|
first_response_output_items: Final[list[object]] = []
|
||||||
for _item in first_response.output:
|
for _item in first_response.output:
|
||||||
if isinstance(_item, dict):
|
if isinstance(_item, dict):
|
||||||
first_response_output_items.append(_item)
|
first_response_output_items.append(_item)
|
||||||
|
|
@ -498,7 +510,7 @@ async def aresponses_with_emulated_file_search(
|
||||||
model: str,
|
model: str,
|
||||||
tools: Iterable[ToolParam] | None = None,
|
tools: Iterable[ToolParam] | None = None,
|
||||||
# Pass-through params — forwarded as-is to the underlying aresponses call
|
# Pass-through params — forwarded as-is to the underlying aresponses call
|
||||||
**kwargs: Any,
|
**kwargs: Any, # kwargs-ok: `object` would surface the caller's partially-unknown dict at its call site
|
||||||
) -> ResponsesAPIResponse:
|
) -> ResponsesAPIResponse:
|
||||||
"""
|
"""
|
||||||
Emulated file_search for providers that don't support it natively.
|
Emulated file_search for providers that don't support it natively.
|
||||||
|
|
@ -507,7 +519,7 @@ async def aresponses_with_emulated_file_search(
|
||||||
runs vector search, and synthesizes an OpenAI-format response.
|
runs vector search, and synthesizes an OpenAI-format response.
|
||||||
"""
|
"""
|
||||||
# Determine whether caller wants search_results populated in the output.
|
# Determine whether caller wants search_results populated in the output.
|
||||||
_include_search_results, kwargs = _prepare_emulated_file_search_call(kwargs=kwargs)
|
_include_search_results, call_kwargs = _prepare_emulated_file_search_call(kwargs=kwargs)
|
||||||
|
|
||||||
# 1. Replace file_search tools with function tool
|
# 1. Replace file_search tools with function tool
|
||||||
transformed_tools, all_vs_ids = _replace_file_search_tools(tools)
|
transformed_tools, all_vs_ids = _replace_file_search_tools(tools)
|
||||||
|
|
@ -524,7 +536,7 @@ async def aresponses_with_emulated_file_search(
|
||||||
input=input,
|
input=input,
|
||||||
model=model,
|
model=model,
|
||||||
tools=transformed_tools or None,
|
tools=transformed_tools or None,
|
||||||
**kwargs,
|
**call_kwargs,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
|
|
@ -588,7 +600,7 @@ async def aresponses_with_emulated_file_search(
|
||||||
input=follow_up_input,
|
input=follow_up_input,
|
||||||
model=model,
|
model=model,
|
||||||
tools=None, # no tools needed for the answer step
|
tools=None, # no tools needed for the answer step
|
||||||
**kwargs,
|
**call_kwargs,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@ logic.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping, Sequence
|
||||||
from typing import Any, Final
|
from typing import Final
|
||||||
|
|
||||||
from pydantic import BaseModel, TypeAdapter, ValidationError
|
from pydantic import BaseModel, TypeAdapter, ValidationError
|
||||||
|
|
||||||
|
|
@ -29,7 +29,7 @@ from litellm.types.llms.openai import (
|
||||||
_MAX_ARGUMENTS_LEN: Final = 1_000_000
|
_MAX_ARGUMENTS_LEN: Final = 1_000_000
|
||||||
|
|
||||||
|
|
||||||
def extract_custom_tool_names(tools: list[Any] | None) -> set[str]:
|
def extract_custom_tool_names(tools: Sequence[object] | None) -> set[str]:
|
||||||
"""Extract names of tools originally defined as ``type: "custom"``."""
|
"""Extract names of tools originally defined as ``type: "custom"``."""
|
||||||
if not tools:
|
if not tools:
|
||||||
return set()
|
return set()
|
||||||
|
|
@ -73,7 +73,7 @@ def build_tool_call_item_kwargs(
|
||||||
arguments_or_input: str,
|
arguments_or_input: str,
|
||||||
status: str,
|
status: str,
|
||||||
custom_tool_names: set[str],
|
custom_tool_names: set[str],
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, str]:
|
||||||
"""Build kwargs for an output item dict that is either a ``function_call``
|
"""Build kwargs for an output item dict that is either a ``function_call``
|
||||||
or a ``custom_tool_call`` depending on whether *name* is in
|
or a ``custom_tool_call`` depending on whether *name* is in
|
||||||
*custom_tool_names*.
|
*custom_tool_names*.
|
||||||
|
|
@ -86,7 +86,7 @@ def build_tool_call_item_kwargs(
|
||||||
"""
|
"""
|
||||||
custom: Final = is_custom_tool_call(name, custom_tool_names)
|
custom: Final = is_custom_tool_call(name, custom_tool_names)
|
||||||
item_type: Final = "custom_tool_call" if custom else "function_call"
|
item_type: Final = "custom_tool_call" if custom else "function_call"
|
||||||
kwargs: Final[dict[str, Any]] = {
|
kwargs: Final[dict[str, str]] = {
|
||||||
"type": item_type,
|
"type": item_type,
|
||||||
"id": call_id,
|
"id": call_id,
|
||||||
"call_id": call_id,
|
"call_id": call_id,
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
Handler for transforming responses api requests to litellm.completion requests
|
Handler for transforming responses api requests to litellm.completion requests
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from collections.abc import Coroutine
|
from collections.abc import Coroutine, Mapping
|
||||||
from typing import Any, Final
|
from typing import Final
|
||||||
|
|
||||||
import litellm
|
import litellm
|
||||||
from litellm.responses.litellm_completion_transformation.streaming_iterator import (
|
from litellm.responses.litellm_completion_transformation.streaming_iterator import (
|
||||||
|
|
@ -30,12 +30,12 @@ class LiteLLMCompletionTransformationHandler:
|
||||||
custom_llm_provider: str | None = None,
|
custom_llm_provider: str | None = None,
|
||||||
_is_async: bool = False,
|
_is_async: bool = False,
|
||||||
stream: bool | None = None,
|
stream: bool | None = None,
|
||||||
extra_headers: dict[str, Any] | None = None,
|
extra_headers: Mapping[str, object] | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> (
|
) -> (
|
||||||
ResponsesAPIResponse
|
ResponsesAPIResponse
|
||||||
| BaseResponsesAPIStreamingIterator
|
| BaseResponsesAPIStreamingIterator
|
||||||
| Coroutine[Any, Any, ResponsesAPIResponse | BaseResponsesAPIStreamingIterator]
|
| Coroutine[object, object, ResponsesAPIResponse | BaseResponsesAPIStreamingIterator]
|
||||||
):
|
):
|
||||||
litellm_completion_request: Final[dict] = (
|
litellm_completion_request: Final[dict] = (
|
||||||
LiteLLMCompletionResponsesConfig.transform_responses_api_request_to_chat_completion_request(
|
LiteLLMCompletionResponsesConfig.transform_responses_api_request_to_chat_completion_request(
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Any, Final, cast
|
||||||
import litellm
|
import litellm
|
||||||
from litellm._logging import verbose_proxy_logger
|
from litellm._logging import verbose_proxy_logger
|
||||||
from litellm.constants import REDACTED_BY_LITELLM, REDACTED_TOOL_CALL_ARGUMENTS_PLACEHOLDER
|
from litellm.constants import REDACTED_BY_LITELLM, REDACTED_TOOL_CALL_ARGUMENTS_PLACEHOLDER
|
||||||
from litellm.proxy._types import SpendLogsPayload
|
from litellm.proxy._types import SpendLogsMetadata, SpendLogsPayload
|
||||||
from litellm.proxy.spend_tracking.cold_storage_handler import ColdStorageHandler
|
from litellm.proxy.spend_tracking.cold_storage_handler import ColdStorageHandler
|
||||||
from litellm.responses.utils import ResponsesAPIRequestUtils
|
from litellm.responses.utils import ResponsesAPIRequestUtils
|
||||||
from litellm.types.llms.openai import (
|
from litellm.types.llms.openai import (
|
||||||
|
|
@ -155,8 +155,8 @@ class ResponsesSessionHandler:
|
||||||
model_response: Final = ModelResponse(**_response_output)
|
model_response: Final = ModelResponse(**_response_output)
|
||||||
for choice in model_response.choices:
|
for choice in model_response.choices:
|
||||||
if hasattr(choice, "message"):
|
if hasattr(choice, "message"):
|
||||||
_normalize_redacted_tool_call_arguments(message := getattr(choice, "message"))
|
_normalize_redacted_tool_call_arguments(choice.message)
|
||||||
chat_completion_message_history.append(message)
|
chat_completion_message_history.append(choice.message)
|
||||||
return chat_completion_message_history
|
return chat_completion_message_history
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
@ -208,7 +208,7 @@ class ResponsesSessionHandler:
|
||||||
try:
|
try:
|
||||||
metadata_str: Final = spend_log.get("metadata", "{}")
|
metadata_str: Final = spend_log.get("metadata", "{}")
|
||||||
if isinstance(metadata_str, str):
|
if isinstance(metadata_str, str):
|
||||||
metadata_dict: Final = json.loads(metadata_str)
|
metadata_dict: Final[SpendLogsMetadata] = json.loads(metadata_str)
|
||||||
return metadata_dict.get("cold_storage_object_key")
|
return metadata_dict.get("cold_storage_object_key")
|
||||||
elif isinstance(metadata_str, dict):
|
elif isinstance(metadata_str, dict):
|
||||||
return metadata_str.get("cold_storage_object_key")
|
return metadata_str.get("cold_storage_object_key")
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
|
from collections.abc import Sequence
|
||||||
from typing import Any, Final, cast
|
from typing import Any, Final, cast
|
||||||
|
|
||||||
import litellm
|
import litellm
|
||||||
|
|
@ -48,14 +49,18 @@ from litellm.types.utils import (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _index_of_output_item_type(items: Sequence[object], item_type: str) -> int | None:
|
||||||
|
return next(
|
||||||
|
(index for index, item in enumerate(items) if getattr(item, "type", None) == item_type),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _output_items_with_id(items: tuple[Any, ...], item_type: str, item_id: str | None) -> tuple[Any, ...]:
|
def _output_items_with_id(items: tuple[Any, ...], item_type: str, item_id: str | None) -> tuple[Any, ...]:
|
||||||
if item_id is None:
|
if item_id is None:
|
||||||
return items
|
return items
|
||||||
|
|
||||||
target_index: Final = next(
|
target_index: Final = _index_of_output_item_type(items, item_type)
|
||||||
(index for index, item in enumerate(items) if getattr(item, "type", None) == item_type),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
if target_index is None:
|
if target_index is None:
|
||||||
return items
|
return items
|
||||||
|
|
||||||
|
|
@ -86,7 +91,7 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator):
|
||||||
self.litellm_metadata: dict | None = litellm_metadata or {}
|
self.litellm_metadata: dict | None = litellm_metadata or {}
|
||||||
# Store lightweight dict snapshots for stream_chunk_builder to reduce
|
# Store lightweight dict snapshots for stream_chunk_builder to reduce
|
||||||
# repeated Pydantic attribute access in end-of-stream assembly.
|
# repeated Pydantic attribute access in end-of-stream assembly.
|
||||||
self.collected_chat_completion_chunks: list[dict[str, Any]] = []
|
self.collected_chat_completion_chunks: list[dict[str, object]] = []
|
||||||
self.finished: bool = False
|
self.finished: bool = False
|
||||||
self.litellm_logging_obj = litellm_custom_stream_wrapper.logging_obj
|
self.litellm_logging_obj = litellm_custom_stream_wrapper.logging_obj
|
||||||
self.sent_response_created_event: bool = False
|
self.sent_response_created_event: bool = False
|
||||||
|
|
@ -98,7 +103,7 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator):
|
||||||
self.sent_output_item_done_event: bool = False
|
self.sent_output_item_done_event: bool = False
|
||||||
self.sent_annotation_events: bool = False
|
self.sent_annotation_events: bool = False
|
||||||
self.litellm_model_response: ModelResponse | TextCompletionResponse | None = None
|
self.litellm_model_response: ModelResponse | TextCompletionResponse | None = None
|
||||||
self.completed_response: Any = None
|
self.completed_response = None
|
||||||
self.final_text: str = ""
|
self.final_text: str = ""
|
||||||
self._cached_item_id: str | None = None
|
self._cached_item_id: str | None = None
|
||||||
self._cached_response_id: str | None = None
|
self._cached_response_id: str | None = None
|
||||||
|
|
@ -123,7 +128,7 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator):
|
||||||
self._reasoning_done_emitted = False
|
self._reasoning_done_emitted = False
|
||||||
self._reasoning_item_id: str | None = None
|
self._reasoning_item_id: str | None = None
|
||||||
self._accumulated_reasoning_content_parts: list[str] = []
|
self._accumulated_reasoning_content_parts: list[str] = []
|
||||||
self._accumulated_provider_specific_fields: dict[str, Any] = {}
|
self._accumulated_provider_specific_fields: dict[str, object] = {}
|
||||||
self._custom_tool_names: set[str] = extract_custom_tool_names(self.responses_api_request.get("tools"))
|
self._custom_tool_names: set[str] = extract_custom_tool_names(self.responses_api_request.get("tools"))
|
||||||
self._namespace_tool_names = LiteLLMCompletionResponsesConfig.namespace_tool_name_map(
|
self._namespace_tool_names = LiteLLMCompletionResponsesConfig.namespace_tool_name_map(
|
||||||
self.responses_api_request.get("tools")
|
self.responses_api_request.get("tools")
|
||||||
|
|
@ -543,7 +548,7 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _snapshot_chunk_for_stream_chunk_builder(
|
def _snapshot_chunk_for_stream_chunk_builder(
|
||||||
chunk: ModelResponseStream,
|
chunk: ModelResponseStream,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, object]:
|
||||||
"""
|
"""
|
||||||
Convert a streaming chunk into a plain dict for end-of-stream assembly.
|
Convert a streaming chunk into a plain dict for end-of-stream assembly.
|
||||||
Keep _hidden_params so downstream usage/header behavior is preserved.
|
Keep _hidden_params so downstream usage/header behavior is preserved.
|
||||||
|
|
@ -1161,7 +1166,7 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator):
|
||||||
if litellm_model_response:
|
if litellm_model_response:
|
||||||
# Add cost to usage object if include_cost_in_streaming_usage is True
|
# Add cost to usage object if include_cost_in_streaming_usage is True
|
||||||
if litellm.include_cost_in_streaming_usage and self.litellm_logging_obj is not None:
|
if litellm.include_cost_in_streaming_usage and self.litellm_logging_obj is not None:
|
||||||
usage: Final = getattr(litellm_model_response, "usage", None)
|
usage: Final[object] = getattr(litellm_model_response, "usage", None)
|
||||||
if usage is not None:
|
if usage is not None:
|
||||||
setattr(
|
setattr(
|
||||||
usage,
|
usage,
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ Handles transforming from Responses API -> LiteLLM completion (Chat Completion
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
from collections.abc import Iterator, Mapping, Sequence
|
from collections.abc import Iterable, Iterator, Mapping, Sequence
|
||||||
from types import MappingProxyType
|
from types import MappingProxyType
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
|
|
@ -28,7 +28,7 @@ from openai.types.responses import ResponseFunctionToolCall
|
||||||
from openai.types.responses.response_create_params import ResponseInputParam
|
from openai.types.responses.response_create_params import ResponseInputParam
|
||||||
from openai.types.responses.tool_param import FunctionToolParam
|
from openai.types.responses.tool_param import FunctionToolParam
|
||||||
from pydantic import TypeAdapter
|
from pydantic import TypeAdapter
|
||||||
from typing_extensions import TypedDict
|
from typing_extensions import ReadOnly, TypedDict
|
||||||
|
|
||||||
from litellm._logging import verbose_logger
|
from litellm._logging import verbose_logger
|
||||||
from litellm.caching import InMemoryCache
|
from litellm.caching import InMemoryCache
|
||||||
|
|
@ -47,6 +47,7 @@ from litellm.types.llms.openai import (
|
||||||
ChatCompletionRedactedThinkingBlock,
|
ChatCompletionRedactedThinkingBlock,
|
||||||
ChatCompletionResponseMessage,
|
ChatCompletionResponseMessage,
|
||||||
ChatCompletionSystemMessage,
|
ChatCompletionSystemMessage,
|
||||||
|
ChatCompletionTextObject,
|
||||||
ChatCompletionThinkingBlock,
|
ChatCompletionThinkingBlock,
|
||||||
ChatCompletionToolCallChunk,
|
ChatCompletionToolCallChunk,
|
||||||
ChatCompletionToolCallFunctionChunk,
|
ChatCompletionToolCallFunctionChunk,
|
||||||
|
|
@ -130,6 +131,30 @@ class _HasId(Protocol):
|
||||||
id: object
|
id: object
|
||||||
|
|
||||||
|
|
||||||
|
class _ResponsesToolCallItem(Protocol):
|
||||||
|
name: object
|
||||||
|
arguments: object
|
||||||
|
|
||||||
|
def get(self, key: str, /) -> object: ...
|
||||||
|
|
||||||
|
|
||||||
|
class _ToolFunctionDefinition(TypedDict, total=False):
|
||||||
|
name: ReadOnly[str]
|
||||||
|
description: ReadOnly[str]
|
||||||
|
parameters: ReadOnly[dict[str, object]]
|
||||||
|
strict: ReadOnly[bool | None]
|
||||||
|
|
||||||
|
|
||||||
|
def _attribute_fields(value: object) -> dict[str, object]:
|
||||||
|
if not hasattr(value, "__dict__"):
|
||||||
|
return {} # mutable-ok: provider_specific_fields payload
|
||||||
|
return dict(cast("Iterable[tuple[str, object]]", value)) # cast-ok: dict() raises on non-pair values, as before
|
||||||
|
|
||||||
|
|
||||||
|
def _input_item_role(input_item: Mapping[str, object]) -> str:
|
||||||
|
return cast(str, input_item.get("role") or "user") # cast-ok: client-supplied role forwarded verbatim, unvalidated
|
||||||
|
|
||||||
|
|
||||||
class ChatCompletionSession(TypedDict, total=False):
|
class ChatCompletionSession(TypedDict, total=False):
|
||||||
messages: list[
|
messages: list[
|
||||||
AllMessageValues
|
AllMessageValues
|
||||||
|
|
@ -678,7 +703,7 @@ class LiteLLMCompletionResponsesConfig:
|
||||||
existing_text: Final = _reasoning_text(msg)
|
existing_text: Final = _reasoning_text(msg)
|
||||||
combined: Final = "\n".join(pending_texts + ((existing_text,) if existing_text else ()))
|
combined: Final = "\n".join(pending_texts + ((existing_text,) if existing_text else ()))
|
||||||
if isinstance(msg, dict):
|
if isinstance(msg, dict):
|
||||||
cast(dict[str, Any], msg)["reasoning_content"] = combined # cast-ok: mutable reasoning carrier
|
cast(dict[str, object], msg)["reasoning_content"] = combined # cast-ok: mutable reasoning carrier
|
||||||
else:
|
else:
|
||||||
setattr(msg, "reasoning_content", combined) # noqa: B010 # attribute name is fixed, not dynamic
|
setattr(msg, "reasoning_content", combined) # noqa: B010 # attribute name is fixed, not dynamic
|
||||||
if pending_blocks:
|
if pending_blocks:
|
||||||
|
|
@ -686,7 +711,7 @@ class LiteLLMCompletionResponsesConfig:
|
||||||
pending_blocks + (_thinking_blocks(msg) or ())
|
pending_blocks + (_thinking_blocks(msg) or ())
|
||||||
)
|
)
|
||||||
if isinstance(msg, dict):
|
if isinstance(msg, dict):
|
||||||
cast(dict[str, Any], msg)["thinking_blocks"] = replayed # cast-ok: mutable reasoning carrier
|
cast(dict[str, object], msg)["thinking_blocks"] = replayed # cast-ok: mutable reasoning carrier
|
||||||
else:
|
else:
|
||||||
setattr(msg, "thinking_blocks", replayed) # noqa: B010 # attribute name is fixed, not dynamic
|
setattr(msg, "thinking_blocks", replayed) # noqa: B010 # attribute name is fixed, not dynamic
|
||||||
|
|
||||||
|
|
@ -1035,7 +1060,7 @@ class LiteLLMCompletionResponsesConfig:
|
||||||
def _add_tool_call_to_assistant(assistant_message: object, tool_call_chunk: ChatCompletionToolCallChunk) -> None:
|
def _add_tool_call_to_assistant(assistant_message: object, tool_call_chunk: ChatCompletionToolCallChunk) -> None:
|
||||||
"""Add a tool_call to an assistant message."""
|
"""Add a tool_call to an assistant message."""
|
||||||
if isinstance(assistant_message, dict):
|
if isinstance(assistant_message, dict):
|
||||||
prev_assistant_dict: Final = cast(dict[str, Any], assistant_message)
|
prev_assistant_dict: Final = cast(dict[str, object], assistant_message)
|
||||||
if "tool_calls" not in prev_assistant_dict:
|
if "tool_calls" not in prev_assistant_dict:
|
||||||
prev_assistant_dict["tool_calls"] = []
|
prev_assistant_dict["tool_calls"] = []
|
||||||
tool_calls_list: Final = prev_assistant_dict["tool_calls"]
|
tool_calls_list: Final = prev_assistant_dict["tool_calls"]
|
||||||
|
|
@ -1120,7 +1145,7 @@ class LiteLLMCompletionResponsesConfig:
|
||||||
# Type-safe way to set tool_call_id on tool message
|
# Type-safe way to set tool_call_id on tool message
|
||||||
if isinstance(message, dict):
|
if isinstance(message, dict):
|
||||||
# Cast to dict to allow setting tool_call_id
|
# Cast to dict to allow setting tool_call_id
|
||||||
message_dict = cast(dict[str, Any], message)
|
message_dict = cast(dict[str, object], message)
|
||||||
message_dict["tool_call_id"] = tool_call_id
|
message_dict["tool_call_id"] = tool_call_id
|
||||||
elif hasattr(message, "tool_call_id"):
|
elif hasattr(message, "tool_call_id"):
|
||||||
setattr(message, "tool_call_id", tool_call_id)
|
setattr(message, "tool_call_id", tool_call_id)
|
||||||
|
|
@ -1172,7 +1197,7 @@ class LiteLLMCompletionResponsesConfig:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _transform_responses_api_input_item_to_chat_completion_message(
|
def _transform_responses_api_input_item_to_chat_completion_message(
|
||||||
input_item: Any,
|
input_item: Mapping[str, object],
|
||||||
replay_reasoning: bool = False,
|
replay_reasoning: bool = False,
|
||||||
) -> list[AllMessageValues | GenericChatCompletionMessage | ChatCompletionResponseMessage]:
|
) -> list[AllMessageValues | GenericChatCompletionMessage | ChatCompletionResponseMessage]:
|
||||||
"""
|
"""
|
||||||
|
|
@ -1200,7 +1225,9 @@ class LiteLLMCompletionResponsesConfig:
|
||||||
elif LiteLLMCompletionResponsesConfig._is_input_item_function_call(input_item):
|
elif LiteLLMCompletionResponsesConfig._is_input_item_function_call(input_item):
|
||||||
# handle function call input items
|
# handle function call input items
|
||||||
return LiteLLMCompletionResponsesConfig._transform_responses_api_function_call_to_chat_completion_message(
|
return LiteLLMCompletionResponsesConfig._transform_responses_api_function_call_to_chat_completion_message(
|
||||||
function_call=input_item
|
function_call=cast( # cast-ok: callee coerces every field it reads with `or ""` / str()
|
||||||
|
Mapping[str, str], input_item
|
||||||
|
)
|
||||||
)
|
)
|
||||||
elif input_item.get("type") == "reasoning":
|
elif input_item.get("type") == "reasoning":
|
||||||
# A ResponseReasoningItemParam carries the prior-turn chain-of-thought.
|
# A ResponseReasoningItemParam carries the prior-turn chain-of-thought.
|
||||||
|
|
@ -1225,7 +1252,7 @@ class LiteLLMCompletionResponsesConfig:
|
||||||
return [] # mutable-ok: empty drop result
|
return [] # mutable-ok: empty drop result
|
||||||
return [ # mutable-ok: single message result
|
return [ # mutable-ok: single message result
|
||||||
GenericChatCompletionMessage(
|
GenericChatCompletionMessage(
|
||||||
role=input_item.get("role") or "user",
|
role=_input_item_role(input_item),
|
||||||
content=LiteLLMCompletionResponsesConfig._transform_responses_api_content_to_chat_completion_content(
|
content=LiteLLMCompletionResponsesConfig._transform_responses_api_content_to_chat_completion_content(
|
||||||
inspectable
|
inspectable
|
||||||
),
|
),
|
||||||
|
|
@ -1253,7 +1280,7 @@ class LiteLLMCompletionResponsesConfig:
|
||||||
return []
|
return []
|
||||||
return [
|
return [
|
||||||
GenericChatCompletionMessage(
|
GenericChatCompletionMessage(
|
||||||
role=input_item.get("role") or "user",
|
role=_input_item_role(input_item),
|
||||||
content=LiteLLMCompletionResponsesConfig._transform_responses_api_content_to_chat_completion_content(
|
content=LiteLLMCompletionResponsesConfig._transform_responses_api_content_to_chat_completion_content(
|
||||||
content
|
content
|
||||||
),
|
),
|
||||||
|
|
@ -1340,7 +1367,7 @@ class LiteLLMCompletionResponsesConfig:
|
||||||
if not isinstance(encrypted_content, str) or not encrypted_content.strip():
|
if not isinstance(encrypted_content, str) or not encrypted_content.strip():
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
decoded: Final[object] = json.loads(encrypted_content)
|
decoded: Final[object] = cast(object, json.loads(encrypted_content)) # cast-ok: json.loads returns Any
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return None
|
return None
|
||||||
if not isinstance(decoded, list):
|
if not isinstance(decoded, list):
|
||||||
|
|
@ -1407,7 +1434,7 @@ class LiteLLMCompletionResponsesConfig:
|
||||||
|
|
||||||
def _normalize_function_call_output_to_tool_content(
|
def _normalize_function_call_output_to_tool_content(
|
||||||
output: object,
|
output: object,
|
||||||
) -> Any:
|
) -> str | list[ChatCompletionTextObject | ChatCompletionImageObject]:
|
||||||
"""
|
"""
|
||||||
Normalize Responses API function_call_output.output into a shape that downstream
|
Normalize Responses API function_call_output.output into a shape that downstream
|
||||||
chat adapters (esp. Gemini) can reliably consume.
|
chat adapters (esp. Gemini) can reliably consume.
|
||||||
|
|
@ -1429,7 +1456,7 @@ class LiteLLMCompletionResponsesConfig:
|
||||||
|
|
||||||
# Some adapters represent tool output as a list of "input_*" parts
|
# Some adapters represent tool output as a list of "input_*" parts
|
||||||
if isinstance(output, list):
|
if isinstance(output, list):
|
||||||
normalized_blocks: Final[list[dict[str, object]]] = []
|
normalized_blocks: Final[list[ChatCompletionTextObject | ChatCompletionImageObject]] = []
|
||||||
text_acc: Final[list[str]] = []
|
text_acc: Final[list[str]] = []
|
||||||
for part in output:
|
for part in output:
|
||||||
if not isinstance(part, dict):
|
if not isinstance(part, dict):
|
||||||
|
|
@ -1903,7 +1930,7 @@ class LiteLLMCompletionResponsesConfig:
|
||||||
result.append(tool)
|
result.append(tool)
|
||||||
continue
|
continue
|
||||||
if tool.get("type") == "function":
|
if tool.get("type") == "function":
|
||||||
fn = cast(dict[str, Any], tool.get("function") or {})
|
fn = cast(_ToolFunctionDefinition, tool.get("function") or {})
|
||||||
parameters = dict(fn.get("parameters", {}) or {})
|
parameters = dict(fn.get("parameters", {}) or {})
|
||||||
if not parameters or "type" not in parameters:
|
if not parameters or "type" not in parameters:
|
||||||
parameters["type"] = "object"
|
parameters["type"] = "object"
|
||||||
|
|
@ -2099,7 +2126,7 @@ class LiteLLMCompletionResponsesConfig:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def convert_response_function_tool_call_to_chat_completion_tool_call(
|
def convert_response_function_tool_call_to_chat_completion_tool_call(
|
||||||
tool_call_item: Any,
|
tool_call_item: object,
|
||||||
index: int = 0,
|
index: int = 0,
|
||||||
) -> dict[str, object]:
|
) -> dict[str, object]:
|
||||||
"""
|
"""
|
||||||
|
|
@ -2112,24 +2139,25 @@ class LiteLLMCompletionResponsesConfig:
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary in ChatCompletionToolCallChunk format
|
Dictionary in ChatCompletionToolCallChunk format
|
||||||
"""
|
"""
|
||||||
|
item: Final = cast( # cast-ok: duck-typed tool call item, .get access guarded by hasattr below
|
||||||
|
_ResponsesToolCallItem, tool_call_item
|
||||||
|
)
|
||||||
# Extract provider_specific_fields if present
|
# Extract provider_specific_fields if present
|
||||||
provider_specific_fields = getattr(tool_call_item, "provider_specific_fields", None)
|
provider_specific_fields: object = getattr(tool_call_item, "provider_specific_fields", None)
|
||||||
if provider_specific_fields and not isinstance(provider_specific_fields, dict):
|
if provider_specific_fields and not isinstance(provider_specific_fields, dict):
|
||||||
provider_specific_fields = (
|
provider_specific_fields = _attribute_fields(provider_specific_fields)
|
||||||
dict(provider_specific_fields) if hasattr(provider_specific_fields, "__dict__") else {}
|
elif hasattr(tool_call_item, "get") and callable(item.get):
|
||||||
)
|
provider_fields: Final = item.get("provider_specific_fields")
|
||||||
elif hasattr(tool_call_item, "get") and callable(tool_call_item.get):
|
|
||||||
provider_fields: Final = tool_call_item.get("provider_specific_fields")
|
|
||||||
if provider_fields:
|
if provider_fields:
|
||||||
provider_specific_fields = (
|
provider_specific_fields = (
|
||||||
provider_fields
|
cast("dict[str, object]", provider_fields) # cast-ok: passed through as-is, keys unvalidated
|
||||||
if isinstance(provider_fields, dict)
|
if isinstance(provider_fields, dict)
|
||||||
else (dict(provider_fields) if hasattr(provider_fields, "__dict__") else {})
|
else _attribute_fields(provider_fields)
|
||||||
)
|
)
|
||||||
|
|
||||||
function_dict: Final[dict[str, object]] = {
|
function_dict: Final[dict[str, object]] = {
|
||||||
"name": tool_call_item.name,
|
"name": item.name,
|
||||||
"arguments": tool_call_item.arguments,
|
"arguments": item.arguments,
|
||||||
}
|
}
|
||||||
|
|
||||||
if provider_specific_fields:
|
if provider_specific_fields:
|
||||||
|
|
@ -2310,7 +2338,7 @@ class LiteLLMCompletionResponsesConfig:
|
||||||
"""
|
"""
|
||||||
output_items: Final[list] = []
|
output_items: Final[list] = []
|
||||||
for choice in chat_completion_response.choices or []:
|
for choice in chat_completion_response.choices or []:
|
||||||
message = getattr(choice, "message", None)
|
message: object = getattr(choice, "message", None)
|
||||||
if not message:
|
if not message:
|
||||||
continue
|
continue
|
||||||
psf = getattr(message, "provider_specific_fields", None)
|
psf = getattr(message, "provider_specific_fields", None)
|
||||||
|
|
@ -2342,7 +2370,7 @@ class LiteLLMCompletionResponsesConfig:
|
||||||
for choice in choices:
|
for choice in choices:
|
||||||
if hasattr(choice, "message") and choice.message:
|
if hasattr(choice, "message") and choice.message:
|
||||||
message = choice.message
|
message = choice.message
|
||||||
reasoning_content = getattr(message, "reasoning_content", None) or ""
|
reasoning_content: str = getattr(message, "reasoning_content", None) or ""
|
||||||
encrypted_content = LiteLLMCompletionResponsesConfig._encode_thinking_blocks(message)
|
encrypted_content = LiteLLMCompletionResponsesConfig._encode_thinking_blocks(message)
|
||||||
if reasoning_content or encrypted_content:
|
if reasoning_content or encrypted_content:
|
||||||
# Only check the first choice for reasoning content
|
# Only check the first choice for reasoning content
|
||||||
|
|
|
||||||
|
|
@ -697,7 +697,7 @@ def _apply_managed_file_id_mapping(
|
||||||
tools = cast(
|
tools = cast(
|
||||||
Iterable[ToolParam] | None,
|
Iterable[ToolParam] | None,
|
||||||
update_responses_tools_with_model_file_ids(
|
update_responses_tools_with_model_file_ids(
|
||||||
tools=cast(list[dict[str, Any]] | None, tools),
|
tools=cast(list[dict[str, object]] | None, tools),
|
||||||
model_id=model_info_id,
|
model_id=model_info_id,
|
||||||
model_file_id_mapping=model_file_id_mapping,
|
model_file_id_mapping=model_file_id_mapping,
|
||||||
),
|
),
|
||||||
|
|
@ -734,7 +734,7 @@ def _responses_try_dispatch_mcp_gateway(
|
||||||
extra_body: dict[str, object] | None,
|
extra_body: dict[str, object] | None,
|
||||||
timeout: float | httpx.Timeout | None,
|
timeout: float | httpx.Timeout | None,
|
||||||
custom_llm_provider: str | None,
|
custom_llm_provider: str | None,
|
||||||
kwargs: dict[str, Any],
|
kwargs: dict[str, object],
|
||||||
_is_async: bool,
|
_is_async: bool,
|
||||||
) -> Any | None:
|
) -> Any | None:
|
||||||
"""Return a response when MCP gateway handles the call; otherwise None."""
|
"""Return a response when MCP gateway handles the call; otherwise None."""
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
"""Helpers for handling MCP-aware `/chat/completions` requests."""
|
"""Helpers for handling MCP-aware `/chat/completions` requests."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, Any, Final, cast
|
from typing import TYPE_CHECKING, Final, cast
|
||||||
|
|
||||||
|
from typing_extensions import TypedDict, Unpack
|
||||||
|
|
||||||
from litellm.responses.mcp.litellm_proxy_mcp_handler import (
|
from litellm.responses.mcp.litellm_proxy_mcp_handler import (
|
||||||
LiteLLM_Proxy_MCP_Handler,
|
LiteLLM_Proxy_MCP_Handler,
|
||||||
|
|
@ -14,6 +16,10 @@ if TYPE_CHECKING:
|
||||||
from litellm.proxy._types import UserAPIKeyAuth
|
from litellm.proxy._types import UserAPIKeyAuth
|
||||||
|
|
||||||
|
|
||||||
|
class _MCPCompletionKwargs(TypedDict, total=False, extra_items=object):
|
||||||
|
"""Extra keywords forwarded verbatim to ``litellm.acompletion``, which owns their contract."""
|
||||||
|
|
||||||
|
|
||||||
def _add_mcp_metadata_to_response(
|
def _add_mcp_metadata_to_response(
|
||||||
response: ModelResponse | CustomStreamWrapper,
|
response: ModelResponse | CustomStreamWrapper,
|
||||||
openai_tools: list | None,
|
openai_tools: list | None,
|
||||||
|
|
@ -79,7 +85,7 @@ async def acompletion_with_mcp(
|
||||||
model: str,
|
model: str,
|
||||||
messages: list,
|
messages: list,
|
||||||
tools: list | None = None,
|
tools: list | None = None,
|
||||||
**kwargs: Any,
|
**kwargs: Unpack[_MCPCompletionKwargs], # kwargs-ok: forwarded verbatim to litellm.acompletion, which owns them
|
||||||
) -> ModelResponse | CustomStreamWrapper:
|
) -> ModelResponse | CustomStreamWrapper:
|
||||||
"""
|
"""
|
||||||
Async completion with MCP integration.
|
Async completion with MCP integration.
|
||||||
|
|
@ -126,7 +132,7 @@ async def acompletion_with_mcp(
|
||||||
) = await LiteLLM_Proxy_MCP_Handler._process_mcp_tools_without_openai_transform(
|
) = await LiteLLM_Proxy_MCP_Handler._process_mcp_tools_without_openai_transform(
|
||||||
user_api_key_auth=user_api_key_auth,
|
user_api_key_auth=user_api_key_auth,
|
||||||
mcp_tools_with_litellm_proxy=mcp_tools_with_litellm_proxy,
|
mcp_tools_with_litellm_proxy=mcp_tools_with_litellm_proxy,
|
||||||
litellm_trace_id=kwargs.get("litellm_trace_id"),
|
litellm_trace_id=context.litellm_trace_id,
|
||||||
mcp_auth_header=mcp_auth_header,
|
mcp_auth_header=mcp_auth_header,
|
||||||
mcp_server_auth_headers=mcp_server_auth_headers,
|
mcp_server_auth_headers=mcp_server_auth_headers,
|
||||||
request_tags=request_tags,
|
request_tags=request_tags,
|
||||||
|
|
@ -168,7 +174,7 @@ async def acompletion_with_mcp(
|
||||||
return response
|
return response
|
||||||
|
|
||||||
# For auto-execute: handle streaming vs non-streaming differently
|
# For auto-execute: handle streaming vs non-streaming differently
|
||||||
stream: Final[bool] = kwargs.get("stream", False)
|
stream: Final[object] = kwargs.get("stream", False)
|
||||||
mock_tool_calls: Final = base_call_args.pop("mock_tool_calls", None)
|
mock_tool_calls: Final = base_call_args.pop("mock_tool_calls", None)
|
||||||
|
|
||||||
if stream:
|
if stream:
|
||||||
|
|
@ -490,8 +496,8 @@ async def acompletion_with_mcp(
|
||||||
mcp_server_auth_headers=mcp_server_auth_headers,
|
mcp_server_auth_headers=mcp_server_auth_headers,
|
||||||
oauth2_headers=oauth2_headers,
|
oauth2_headers=oauth2_headers,
|
||||||
raw_headers=raw_headers,
|
raw_headers=raw_headers,
|
||||||
litellm_call_id=kwargs.get("litellm_call_id"),
|
litellm_call_id=context.litellm_call_id,
|
||||||
litellm_trace_id=kwargs.get("litellm_trace_id"),
|
litellm_trace_id=context.litellm_trace_id,
|
||||||
openai_tools=openai_tools,
|
openai_tools=openai_tools,
|
||||||
base_call_args=base_call_args,
|
base_call_args=base_call_args,
|
||||||
request_tags=request_tags,
|
request_tags=request_tags,
|
||||||
|
|
@ -604,8 +610,8 @@ async def acompletion_with_mcp(
|
||||||
mcp_server_auth_headers=mcp_server_auth_headers,
|
mcp_server_auth_headers=mcp_server_auth_headers,
|
||||||
oauth2_headers=oauth2_headers,
|
oauth2_headers=oauth2_headers,
|
||||||
raw_headers=raw_headers,
|
raw_headers=raw_headers,
|
||||||
litellm_call_id=kwargs.get("litellm_call_id"),
|
litellm_call_id=context.litellm_call_id,
|
||||||
litellm_trace_id=kwargs.get("litellm_trace_id"),
|
litellm_trace_id=context.litellm_trace_id,
|
||||||
request_tags=request_tags,
|
request_tags=request_tags,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ from litellm.types.llms.openai import (
|
||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import AsyncIterator, Iterator
|
||||||
|
|
||||||
from mcp.types import Tool as MCPTool
|
from mcp.types import Tool as MCPTool
|
||||||
|
|
||||||
from litellm.proxy._types import UserAPIKeyAuth
|
from litellm.proxy._types import UserAPIKeyAuth
|
||||||
|
|
@ -511,7 +513,9 @@ class MCPEnhancedStreamingIterator(BaseResponsesAPIStreamingIterator):
|
||||||
if self.base_iterator:
|
if self.base_iterator:
|
||||||
if hasattr(self.base_iterator, "__anext__"):
|
if hasattr(self.base_iterator, "__anext__"):
|
||||||
try:
|
try:
|
||||||
chunk: Final[ResponsesAPIStreamingResponse] = await cast(Any, self.base_iterator).__anext__()
|
chunk: Final[ResponsesAPIStreamingResponse] = await cast( # cast-ok: hasattr __anext__ checked
|
||||||
|
"AsyncIterator[ResponsesAPIStreamingResponse]", self.base_iterator
|
||||||
|
).__anext__()
|
||||||
|
|
||||||
# Capture the response ID from the first event to ensure consistency
|
# Capture the response ID from the first event to ensure consistency
|
||||||
if self._cached_response_id is None and hasattr(chunk, "response"):
|
if self._cached_response_id is None and hasattr(chunk, "response"):
|
||||||
|
|
@ -569,7 +573,9 @@ class MCPEnhancedStreamingIterator(BaseResponsesAPIStreamingIterator):
|
||||||
if not self.base_iterator or not hasattr(self.base_iterator, "__anext__"):
|
if not self.base_iterator or not hasattr(self.base_iterator, "__anext__"):
|
||||||
raise StopAsyncIteration
|
raise StopAsyncIteration
|
||||||
|
|
||||||
chunk: Final[ResponsesAPIStreamingResponse] = await cast(Any, self.base_iterator).__anext__()
|
chunk: Final[ResponsesAPIStreamingResponse] = await cast( # cast-ok: hasattr __anext__ checked above
|
||||||
|
"AsyncIterator[ResponsesAPIStreamingResponse]", self.base_iterator
|
||||||
|
).__anext__()
|
||||||
|
|
||||||
if self._cached_response_id is None and hasattr(chunk, "response"):
|
if self._cached_response_id is None and hasattr(chunk, "response"):
|
||||||
new_response: Final[ResponsesAPIResponse | None] = getattr(chunk, "response", None)
|
new_response: Final[ResponsesAPIResponse | None] = getattr(chunk, "response", None)
|
||||||
|
|
@ -834,7 +840,9 @@ class MCPEnhancedStreamingIterator(BaseResponsesAPIStreamingIterator):
|
||||||
if not self.is_async:
|
if not self.is_async:
|
||||||
try:
|
try:
|
||||||
if self.base_iterator and hasattr(self.base_iterator, "__next__"):
|
if self.base_iterator and hasattr(self.base_iterator, "__next__"):
|
||||||
return next(cast(Any, self.base_iterator))
|
return next(
|
||||||
|
cast("Iterator[ResponsesAPIStreamingResponse]", self.base_iterator) # cast-ok: hasattr-checked
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise StopIteration
|
raise StopIteration
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,25 @@ still executes the tool, just with no credentials.
|
||||||
|
|
||||||
from collections.abc import Iterable, Mapping, Sequence
|
from collections.abc import Iterable, Mapping, Sequence
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Final
|
from typing import TYPE_CHECKING, Any, Final
|
||||||
|
|
||||||
|
from typing_extensions import NotRequired, ReadOnly, TypedDict
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from litellm.proxy._types import UserAPIKeyAuth
|
||||||
|
|
||||||
|
|
||||||
|
class _AuthCarryingMetadata(TypedDict):
|
||||||
|
"""The one key this module reads out of a request's ``metadata`` / ``litellm_metadata``."""
|
||||||
|
|
||||||
|
user_api_key_auth: ReadOnly[NotRequired["UserAPIKeyAuth | None"]]
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class MCPRequestContext:
|
class MCPRequestContext:
|
||||||
"""Everything a gateway handler must forward to MCP tool listing and execution."""
|
"""Everything a gateway handler must forward to MCP tool listing and execution."""
|
||||||
|
|
||||||
user_api_key_auth: Any # any-ok: UserAPIKeyAuth is proxy-only; importing it here would create a cycle
|
user_api_key_auth: "UserAPIKeyAuth | None"
|
||||||
mcp_auth_header: str | None = None
|
mcp_auth_header: str | None = None
|
||||||
mcp_server_auth_headers: Mapping[str, Mapping[str, str]] | None = None
|
mcp_server_auth_headers: Mapping[str, Mapping[str, str]] | None = None
|
||||||
oauth2_headers: Mapping[str, str] | None = None
|
oauth2_headers: Mapping[str, str] | None = None
|
||||||
|
|
@ -30,7 +41,7 @@ class MCPRequestContext:
|
||||||
def resolve(
|
def resolve(
|
||||||
cls,
|
cls,
|
||||||
kwargs: Mapping[str, Any],
|
kwargs: Mapping[str, Any],
|
||||||
tools: Iterable[Any] | None,
|
tools: Iterable[object] | None,
|
||||||
) -> "MCPRequestContext":
|
) -> "MCPRequestContext":
|
||||||
"""
|
"""
|
||||||
Build the context from a gateway handler's kwargs.
|
Build the context from a gateway handler's kwargs.
|
||||||
|
|
@ -44,9 +55,9 @@ class MCPRequestContext:
|
||||||
)
|
)
|
||||||
from litellm.responses.utils import ResponsesAPIRequestUtils
|
from litellm.responses.utils import ResponsesAPIRequestUtils
|
||||||
|
|
||||||
litellm_metadata: Final = kwargs.get("litellm_metadata") or {}
|
litellm_metadata: Final[_AuthCarryingMetadata] = kwargs.get("litellm_metadata") or {}
|
||||||
metadata: Final = kwargs.get("metadata") or {}
|
metadata: Final[_AuthCarryingMetadata] = kwargs.get("metadata") or {}
|
||||||
user_api_key_auth: Final = (
|
user_api_key_auth: Final[UserAPIKeyAuth | None] = (
|
||||||
kwargs.get("user_api_key_auth")
|
kwargs.get("user_api_key_auth")
|
||||||
or litellm_metadata.get("user_api_key_auth")
|
or litellm_metadata.get("user_api_key_auth")
|
||||||
or metadata.get("user_api_key_auth")
|
or metadata.get("user_api_key_auth")
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,17 @@ caller automatically applies to all of them.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from typing import Any, Final
|
from collections.abc import Mapping
|
||||||
|
from typing import Final, SupportsInt, TypeAlias, cast # noqa: TID251 # int() re-checks the cast below at runtime
|
||||||
|
|
||||||
from litellm.constants import STREAM_SSE_DONE_STRING
|
from litellm.constants import STREAM_SSE_DONE_STRING
|
||||||
|
|
||||||
_MAX_CONTENT_INDEX: Final = 1024
|
_MAX_CONTENT_INDEX: Final = 1024
|
||||||
|
|
||||||
|
_ConvertibleToInt: TypeAlias = SupportsInt | str
|
||||||
|
|
||||||
def parse_sse_json_chunk(chunk: str) -> dict[str, Any] | None:
|
|
||||||
|
def parse_sse_json_chunk(chunk: str) -> dict[str, object] | None:
|
||||||
"""Parse a single raw SSE line into a JSON object dict.
|
"""Parse a single raw SSE line into a JSON object dict.
|
||||||
|
|
||||||
Returns ``None`` for empty lines, ``event:`` lines, ``[DONE]`` markers,
|
Returns ``None`` for empty lines, ``event:`` lines, ``[DONE]`` markers,
|
||||||
|
|
@ -30,7 +33,7 @@ def parse_sse_json_chunk(chunk: str) -> dict[str, Any] | None:
|
||||||
if not stripped_chunk or stripped_chunk == STREAM_SSE_DONE_STRING or stripped_chunk.startswith("event:"):
|
if not stripped_chunk or stripped_chunk == STREAM_SSE_DONE_STRING or stripped_chunk.startswith("event:"):
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
parsed_chunk: Final = json.loads(stripped_chunk)
|
parsed_chunk: Final[object] = json.loads(stripped_chunk)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
return None
|
return None
|
||||||
if not isinstance(parsed_chunk, dict):
|
if not isinstance(parsed_chunk, dict):
|
||||||
|
|
@ -38,9 +41,19 @@ def parse_sse_json_chunk(chunk: str) -> dict[str, Any] | None:
|
||||||
return parsed_chunk
|
return parsed_chunk
|
||||||
|
|
||||||
|
|
||||||
|
def _chunk_index(parsed_chunk: Mapping[str, object], key: str, fallback: int) -> int:
|
||||||
|
raw_index: Final = parsed_chunk.get(key)
|
||||||
|
if raw_index is None:
|
||||||
|
return fallback
|
||||||
|
try:
|
||||||
|
return int(cast(_ConvertibleToInt, raw_index)) # cast-ok: int() raises TypeError otherwise, caught below
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return fallback
|
||||||
|
|
||||||
|
|
||||||
def record_output_item_chunk(
|
def record_output_item_chunk(
|
||||||
parsed_chunk: dict[str, Any],
|
parsed_chunk: Mapping[str, object],
|
||||||
output_items: dict[int, dict[str, Any]],
|
output_items: dict[int, dict[str, object]],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Record an OUTPUT_ITEM_DONE chunk into ``output_items`` keyed by
|
"""Record an OUTPUT_ITEM_DONE chunk into ``output_items`` keyed by
|
||||||
``output_index`` (falling back to the next free slot when missing).
|
``output_index`` (falling back to the next free slot when missing).
|
||||||
|
|
@ -48,20 +61,14 @@ def record_output_item_chunk(
|
||||||
item: Final = parsed_chunk.get("item")
|
item: Final = parsed_chunk.get("item")
|
||||||
if not isinstance(item, dict):
|
if not isinstance(item, dict):
|
||||||
return
|
return
|
||||||
try:
|
output_index: Final = _chunk_index(parsed_chunk, "output_index", len(output_items))
|
||||||
output_index_raw: Final = parsed_chunk.get("output_index")
|
|
||||||
if output_index_raw is None:
|
|
||||||
raise ValueError("missing output_index")
|
|
||||||
output_index = int(output_index_raw)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
output_index = len(output_items)
|
|
||||||
output_items[output_index] = item
|
output_items[output_index] = item
|
||||||
|
|
||||||
|
|
||||||
def record_output_text_chunk(
|
def record_output_text_chunk(
|
||||||
parsed_chunk: dict[str, Any],
|
parsed_chunk: Mapping[str, object],
|
||||||
output_items: dict[int, dict[str, Any]],
|
output_items: Mapping[int, dict[str, object]],
|
||||||
text_only_items: dict[int, dict[str, Any]],
|
text_only_items: dict[int, dict[str, object]],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Record an OUTPUT_TEXT_DONE chunk as a synthetic message item in
|
"""Record an OUTPUT_TEXT_DONE chunk as a synthetic message item in
|
||||||
``text_only_items``. Real OUTPUT_ITEM_DONE events already captured in
|
``text_only_items``. Real OUTPUT_ITEM_DONE events already captured in
|
||||||
|
|
@ -71,13 +78,7 @@ def record_output_text_chunk(
|
||||||
if not isinstance(text, str):
|
if not isinstance(text, str):
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
output_index: Final = _chunk_index(parsed_chunk, "output_index", len(text_only_items))
|
||||||
output_index_raw: Final = parsed_chunk.get("output_index")
|
|
||||||
if output_index_raw is None:
|
|
||||||
raise ValueError("missing output_index")
|
|
||||||
output_index = int(output_index_raw)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
output_index = len(text_only_items)
|
|
||||||
|
|
||||||
if output_index in output_items:
|
if output_index in output_items:
|
||||||
return
|
return
|
||||||
|
|
@ -97,13 +98,7 @@ def record_output_text_chunk(
|
||||||
if not isinstance(content, list):
|
if not isinstance(content, list):
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
content_index: Final = _chunk_index(parsed_chunk, "content_index", len(content))
|
||||||
content_index_raw: Final = parsed_chunk.get("content_index")
|
|
||||||
if content_index_raw is None:
|
|
||||||
raise ValueError("missing content_index")
|
|
||||||
content_index = int(content_index_raw)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
content_index = len(content)
|
|
||||||
|
|
||||||
if content_index < 0 or content_index > _MAX_CONTENT_INDEX:
|
if content_index < 0 or content_index > _MAX_CONTENT_INDEX:
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,21 @@ if TYPE_CHECKING:
|
||||||
ResponsesClientWebSocket,
|
ResponsesClientWebSocket,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class _StreamCachingHandler(Protocol):
|
||||||
|
"""The ``_llm_caching_handler`` attached to a logging object, as this module uses it."""
|
||||||
|
|
||||||
|
original_function: Callable[..., object]
|
||||||
|
|
||||||
|
def _should_store_result_in_cache(
|
||||||
|
self, original_function: Callable[..., object], kwargs: Mapping[str, object]
|
||||||
|
) -> bool: ...
|
||||||
|
|
||||||
|
class PiiUnmaskingGuardrailCallback(PresidioGuardrailCallback, Protocol):
|
||||||
|
"""Guardrail callback that can also reverse its own masking, selected by
|
||||||
|
``llm_http_handler`` on exactly this attribute."""
|
||||||
|
|
||||||
|
def _unmask_pii_text(self, text: str, pii_tokens: Mapping[str, str]) -> str: ...
|
||||||
|
|
||||||
|
|
||||||
class ProjectQuotaCallback(Protocol):
|
class ProjectQuotaCallback(Protocol):
|
||||||
async def enforce_project_io_token_quota_for_frame(
|
async def enforce_project_io_token_quota_for_frame(
|
||||||
|
|
@ -84,6 +99,11 @@ def _load_json_object(payload: str | bytes) -> dict[str, object]:
|
||||||
return json.loads(payload)
|
return json.loads(payload)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_json_value(payload: str | bytes) -> object:
|
||||||
|
"""Parse a JSON payload whose top-level shape the caller narrows itself."""
|
||||||
|
return json.loads(payload)
|
||||||
|
|
||||||
|
|
||||||
def _model_id_from_metadata(litellm_metadata: dict[str, object] | None) -> str | None:
|
def _model_id_from_metadata(litellm_metadata: dict[str, object] | None) -> str | None:
|
||||||
model_info: Final = litellm_metadata.get("model_info") if litellm_metadata else None
|
model_info: Final = litellm_metadata.get("model_info") if litellm_metadata else None
|
||||||
model_id: Final = model_info.get("id") if _is_json_object(model_info) else None
|
model_id: Final = model_info.get("id") if _is_json_object(model_info) else None
|
||||||
|
|
@ -243,10 +263,10 @@ class BaseResponsesAPIStreamingIterator:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Parse the JSON chunk
|
# Parse the JSON chunk
|
||||||
parsed_chunk: Final = json.loads(chunk)
|
parsed_chunk: Final = _load_json_value(chunk)
|
||||||
|
|
||||||
# Format as ResponsesAPIStreamingResponse
|
# Format as ResponsesAPIStreamingResponse
|
||||||
if isinstance(parsed_chunk, dict):
|
if _is_json_object(parsed_chunk):
|
||||||
if self.responses_api_provider_config is None:
|
if self.responses_api_provider_config is None:
|
||||||
raise ValueError("responses_api_provider_config is required to process live streaming chunks")
|
raise ValueError("responses_api_provider_config is required to process live streaming chunks")
|
||||||
openai_responses_api_chunk: Final = self.responses_api_provider_config.transform_streaming_response(
|
openai_responses_api_chunk: Final = self.responses_api_provider_config.transform_streaming_response(
|
||||||
|
|
@ -529,7 +549,7 @@ class BaseResponsesAPIStreamingIterator:
|
||||||
if response_obj is None:
|
if response_obj is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
caching_handler: Final = getattr(self.logging_obj, "_llm_caching_handler", None)
|
caching_handler: Final[_StreamCachingHandler | None] = getattr(self.logging_obj, "_llm_caching_handler", None)
|
||||||
if caching_handler is None:
|
if caching_handler is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -547,7 +567,7 @@ class BaseResponsesAPIStreamingIterator:
|
||||||
if preset_cache_key is not None:
|
if preset_cache_key is not None:
|
||||||
request_kwargs["cache_key"] = preset_cache_key
|
request_kwargs["cache_key"] = preset_cache_key
|
||||||
|
|
||||||
if not caching_handler._should_store_result_in_cache(
|
if not caching_handler._should_store_result_in_cache( # pyright: ignore[reportPrivateUsage] # no public API
|
||||||
original_function=caching_handler.original_function,
|
original_function=caching_handler.original_function,
|
||||||
kwargs=request_kwargs,
|
kwargs=request_kwargs,
|
||||||
):
|
):
|
||||||
|
|
@ -1401,7 +1421,7 @@ async def _enforce_frame_project_quota(
|
||||||
if not quota_callbacks:
|
if not quota_callbacks:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
msg_obj = json.loads(raw_message)
|
msg_obj: Final = _load_json_value(raw_message)
|
||||||
except (json.JSONDecodeError, TypeError):
|
except (json.JSONDecodeError, TypeError):
|
||||||
return
|
return
|
||||||
if not _is_json_object(msg_obj) or msg_obj.get("type") != "response.create":
|
if not _is_json_object(msg_obj) or msg_obj.get("type") != "response.create":
|
||||||
|
|
@ -1451,7 +1471,7 @@ class ResponsesWebSocketStreaming:
|
||||||
user_api_key_dict: UserAPIKeyAuth | None = None,
|
user_api_key_dict: UserAPIKeyAuth | None = None,
|
||||||
request_data: dict[str, object] | None = None,
|
request_data: dict[str, object] | None = None,
|
||||||
first_message: str | None = None,
|
first_message: str | None = None,
|
||||||
guardrail_callbacks: list[Any] | None = None,
|
guardrail_callbacks: list[PiiUnmaskingGuardrailCallback] | None = None,
|
||||||
output_guardrail_callbacks: list[PresidioGuardrailCallback] | None = None,
|
output_guardrail_callbacks: list[PresidioGuardrailCallback] | None = None,
|
||||||
quota_callbacks: Sequence[ProjectQuotaCallback] | None = None,
|
quota_callbacks: Sequence[ProjectQuotaCallback] | None = None,
|
||||||
authorized_model: str | None = None,
|
authorized_model: str | None = None,
|
||||||
|
|
@ -1464,7 +1484,7 @@ class ResponsesWebSocketStreaming:
|
||||||
self.messages: list[dict[str, object]] = []
|
self.messages: list[dict[str, object]] = []
|
||||||
self.input_messages: list[dict[str, object]] = []
|
self.input_messages: list[dict[str, object]] = []
|
||||||
self.first_message = first_message
|
self.first_message = first_message
|
||||||
self.guardrail_callbacks: list[Any] = guardrail_callbacks or []
|
self.guardrail_callbacks: list[PiiUnmaskingGuardrailCallback] = guardrail_callbacks or []
|
||||||
self.output_guardrail_callbacks: list[PresidioGuardrailCallback] = output_guardrail_callbacks or []
|
self.output_guardrail_callbacks: list[PresidioGuardrailCallback] = output_guardrail_callbacks or []
|
||||||
self.quota_callbacks: tuple[ProjectQuotaCallback, ...] = tuple(quota_callbacks) if quota_callbacks else ()
|
self.quota_callbacks: tuple[ProjectQuotaCallback, ...] = tuple(quota_callbacks) if quota_callbacks else ()
|
||||||
# Model name authorized at connection time; enforced on every
|
# Model name authorized at connection time; enforced on every
|
||||||
|
|
@ -1780,7 +1800,9 @@ class ResponsesWebSocketStreaming:
|
||||||
continue
|
continue
|
||||||
text = content_block.get("text")
|
text = content_block.get("text")
|
||||||
if isinstance(text, str):
|
if isinstance(text, str):
|
||||||
unmasked = cb._unmask_pii_text(text, pii_tokens)
|
unmasked = cb._unmask_pii_text( # pyright: ignore[reportPrivateUsage] # no public unmasker
|
||||||
|
text, pii_tokens
|
||||||
|
)
|
||||||
if unmasked != text:
|
if unmasked != text:
|
||||||
content_block["text"] = unmasked
|
content_block["text"] = unmasked
|
||||||
modified = True
|
modified = True
|
||||||
|
|
@ -1789,7 +1811,9 @@ class ResponsesWebSocketStreaming:
|
||||||
if event_type in self._DELTA_EVENT_TYPES:
|
if event_type in self._DELTA_EVENT_TYPES:
|
||||||
delta: Final = evt_obj.get("delta")
|
delta: Final = evt_obj.get("delta")
|
||||||
if isinstance(delta, str):
|
if isinstance(delta, str):
|
||||||
unmasked = cb._unmask_pii_text(delta, pii_tokens)
|
unmasked = cb._unmask_pii_text( # pyright: ignore[reportPrivateUsage] # no public unmasker
|
||||||
|
delta, pii_tokens
|
||||||
|
)
|
||||||
if unmasked != delta:
|
if unmasked != delta:
|
||||||
evt_obj["delta"] = unmasked
|
evt_obj["delta"] = unmasked
|
||||||
return json.dumps(evt_obj)
|
return json.dumps(evt_obj)
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
import base64
|
import base64
|
||||||
import re
|
import re
|
||||||
from collections.abc import Iterable, Mapping
|
from collections.abc import Iterable, Mapping, Sequence
|
||||||
from typing import Any, Final, Optional, Union, cast, get_type_hints, overload
|
from typing import Any, Final, Optional, Union, cast, get_type_hints, overload
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
from typing_extensions import TypeIs # noqa: TID251 # narrows untyped wire payloads without a runtime conversion
|
||||||
|
|
||||||
import litellm
|
import litellm
|
||||||
from litellm._logging import verbose_logger
|
from litellm._logging import verbose_logger
|
||||||
from litellm.llms.base_llm.responses.transformation import BaseResponsesAPIConfig
|
from litellm.llms.base_llm.responses.transformation import BaseResponsesAPIConfig
|
||||||
from litellm.types.llms.openai import (
|
from litellm.types.llms.openai import (
|
||||||
AllMessageValues,
|
AllMessageValues,
|
||||||
|
OutputTokensDetails,
|
||||||
ResponseAPIUsage,
|
ResponseAPIUsage,
|
||||||
ResponseInputParam,
|
ResponseInputParam,
|
||||||
ResponsesAPIOptionalRequestParams,
|
ResponsesAPIOptionalRequestParams,
|
||||||
|
|
@ -26,6 +28,16 @@ from litellm.types.utils import (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_object_sequence(value: object) -> TypeIs[Sequence[object]]: # guard-ok: a list is a Sequence of anything
|
||||||
|
return isinstance(value, list)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_object_dict(
|
||||||
|
value: object,
|
||||||
|
) -> TypeIs[dict[str, object]]: # guard-ok: wire dicts have str keys # mutable-ok: callers rewrite ids in place
|
||||||
|
return isinstance(value, dict)
|
||||||
|
|
||||||
|
|
||||||
def normalize_responses_api_stream_options(
|
def normalize_responses_api_stream_options(
|
||||||
stream_options: object,
|
stream_options: object,
|
||||||
) -> ResponsesAPIStreamOptions | None:
|
) -> ResponsesAPIStreamOptions | None:
|
||||||
|
|
@ -703,12 +715,12 @@ class ResponsesAPIRequestUtils:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _encode_container_ids_in_annotations(
|
def _encode_container_ids_in_annotations(
|
||||||
annotations: Any,
|
annotations: object,
|
||||||
custom_llm_provider: str | None,
|
custom_llm_provider: str | None,
|
||||||
model_id: str | None,
|
model_id: str | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Encode ``container_id`` on each annotation (e.g. ``container_file_citation``)."""
|
"""Encode ``container_id`` on each annotation (e.g. ``container_file_citation``)."""
|
||||||
if not annotations or not isinstance(annotations, list):
|
if not annotations or not _is_object_sequence(annotations):
|
||||||
return
|
return
|
||||||
for ann in annotations:
|
for ann in annotations:
|
||||||
ResponsesAPIRequestUtils._encode_container_id_on_output_item(
|
ResponsesAPIRequestUtils._encode_container_id_on_output_item(
|
||||||
|
|
@ -719,16 +731,16 @@ class ResponsesAPIRequestUtils:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _encode_container_ids_in_message_content(
|
def _encode_container_ids_in_message_content(
|
||||||
content: Any,
|
content: object,
|
||||||
custom_llm_provider: str | None,
|
custom_llm_provider: str | None,
|
||||||
model_id: str | None,
|
model_id: str | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Walk message ``content`` parts and encode citation ``container_id`` values."""
|
"""Walk message ``content`` parts and encode citation ``container_id`` values."""
|
||||||
if not content:
|
if not content:
|
||||||
return
|
return
|
||||||
if isinstance(content, list):
|
if _is_object_sequence(content):
|
||||||
for part in content:
|
for part in content:
|
||||||
if isinstance(part, dict):
|
if _is_object_dict(part):
|
||||||
ResponsesAPIRequestUtils._encode_container_ids_in_annotations(
|
ResponsesAPIRequestUtils._encode_container_ids_in_annotations(
|
||||||
part.get("annotations"),
|
part.get("annotations"),
|
||||||
custom_llm_provider,
|
custom_llm_provider,
|
||||||
|
|
@ -743,7 +755,7 @@ class ResponsesAPIRequestUtils:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _encode_container_id_on_output_item(
|
def _encode_container_id_on_output_item(
|
||||||
item: Any,
|
item: object,
|
||||||
custom_llm_provider: str | None,
|
custom_llm_provider: str | None,
|
||||||
model_id: str | None,
|
model_id: str | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
@ -770,14 +782,14 @@ class ResponsesAPIRequestUtils:
|
||||||
container_id=container_id,
|
container_id=container_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
if isinstance(item, dict):
|
if _is_object_dict(item):
|
||||||
cid: Final = item.get("container_id")
|
cid: Final = item.get("container_id")
|
||||||
if isinstance(cid, str):
|
if isinstance(cid, str):
|
||||||
enc = _maybe_encode(cid)
|
enc = _maybe_encode(cid)
|
||||||
if enc is not None:
|
if enc is not None:
|
||||||
item["container_id"] = enc
|
item["container_id"] = enc # rebind-ok: this helper's contract is to rewrite the item in place
|
||||||
nested: Final = item.get("code_interpreter_call")
|
nested: Final = item.get("code_interpreter_call")
|
||||||
if isinstance(nested, dict):
|
if _is_object_dict(nested):
|
||||||
nc: Final = nested.get("container_id")
|
nc: Final = nested.get("container_id")
|
||||||
if isinstance(nc, str):
|
if isinstance(nc, str):
|
||||||
enc = _maybe_encode(nc)
|
enc = _maybe_encode(nc)
|
||||||
|
|
@ -803,7 +815,7 @@ class ResponsesAPIRequestUtils:
|
||||||
exc_info=True,
|
exc_info=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
nested_obj: Final = getattr(item, "code_interpreter_call", None)
|
nested_obj: Final[object] = getattr(item, "code_interpreter_call", None)
|
||||||
if nested_obj is not None:
|
if nested_obj is not None:
|
||||||
ResponsesAPIRequestUtils._encode_container_id_on_output_item(
|
ResponsesAPIRequestUtils._encode_container_id_on_output_item(
|
||||||
nested_obj,
|
nested_obj,
|
||||||
|
|
@ -820,24 +832,24 @@ class ResponsesAPIRequestUtils:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _collect_container_ids_from_annotations(
|
def _collect_container_ids_from_annotations(
|
||||||
annotations: Any,
|
annotations: object,
|
||||||
collected: set[str],
|
collected: set[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
if not annotations or not isinstance(annotations, list):
|
if not annotations or not _is_object_sequence(annotations):
|
||||||
return
|
return
|
||||||
for ann in annotations:
|
for ann in annotations:
|
||||||
ResponsesAPIRequestUtils._collect_container_ids_from_output_item(ann, collected)
|
ResponsesAPIRequestUtils._collect_container_ids_from_output_item(ann, collected)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _collect_container_ids_from_message_content(
|
def _collect_container_ids_from_message_content(
|
||||||
content: Any,
|
content: object,
|
||||||
collected: set[str],
|
collected: set[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
if not content:
|
if not content:
|
||||||
return
|
return
|
||||||
if isinstance(content, list):
|
if _is_object_sequence(content):
|
||||||
for part in content:
|
for part in content:
|
||||||
if isinstance(part, dict):
|
if _is_object_dict(part):
|
||||||
ResponsesAPIRequestUtils._collect_container_ids_from_annotations(
|
ResponsesAPIRequestUtils._collect_container_ids_from_annotations(
|
||||||
part.get("annotations"),
|
part.get("annotations"),
|
||||||
collected,
|
collected,
|
||||||
|
|
@ -850,19 +862,19 @@ class ResponsesAPIRequestUtils:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _collect_container_ids_from_output_item(
|
def _collect_container_ids_from_output_item(
|
||||||
item: Any,
|
item: object,
|
||||||
collected: set[str],
|
collected: set[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Collect managed or raw ``container_id`` values from one output item."""
|
"""Collect managed or raw ``container_id`` values from one output item."""
|
||||||
if item is None:
|
if item is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
if isinstance(item, dict):
|
if _is_object_dict(item):
|
||||||
cid: Final = item.get("container_id")
|
cid: Final = item.get("container_id")
|
||||||
if isinstance(cid, str) and cid:
|
if isinstance(cid, str) and cid:
|
||||||
collected.add(cid)
|
collected.add(cid)
|
||||||
nested: Final = item.get("code_interpreter_call")
|
nested: Final = item.get("code_interpreter_call")
|
||||||
if isinstance(nested, dict):
|
if _is_object_dict(nested):
|
||||||
nc: Final = nested.get("container_id")
|
nc: Final = nested.get("container_id")
|
||||||
if isinstance(nc, str) and nc:
|
if isinstance(nc, str) and nc:
|
||||||
collected.add(nc)
|
collected.add(nc)
|
||||||
|
|
@ -877,7 +889,7 @@ class ResponsesAPIRequestUtils:
|
||||||
if isinstance(cid_attr, str) and cid_attr:
|
if isinstance(cid_attr, str) and cid_attr:
|
||||||
collected.add(cid_attr)
|
collected.add(cid_attr)
|
||||||
|
|
||||||
nested_obj: Final = getattr(item, "code_interpreter_call", None)
|
nested_obj: Final[object] = getattr(item, "code_interpreter_call", None)
|
||||||
if nested_obj is not None:
|
if nested_obj is not None:
|
||||||
ResponsesAPIRequestUtils._collect_container_ids_from_output_item(nested_obj, collected)
|
ResponsesAPIRequestUtils._collect_container_ids_from_output_item(nested_obj, collected)
|
||||||
|
|
||||||
|
|
@ -1108,7 +1120,9 @@ class ResponseAPILoggingUtils:
|
||||||
cache_write_tokens=getattr(response_api_usage.input_tokens_details, "cache_write_tokens", None),
|
cache_write_tokens=getattr(response_api_usage.input_tokens_details, "cache_write_tokens", None),
|
||||||
)
|
)
|
||||||
completion_tokens_details: CompletionTokensDetailsWrapper | None = None
|
completion_tokens_details: CompletionTokensDetailsWrapper | None = None
|
||||||
output_tokens_details: Final = getattr(response_api_usage, "output_tokens_details", None)
|
output_tokens_details: Final[OutputTokensDetails | None] = getattr(
|
||||||
|
response_api_usage, "output_tokens_details", None
|
||||||
|
)
|
||||||
if output_tokens_details:
|
if output_tokens_details:
|
||||||
completion_tokens_details = CompletionTokensDetailsWrapper(
|
completion_tokens_details = CompletionTokensDetailsWrapper(
|
||||||
reasoning_tokens=getattr(output_tokens_details, "reasoning_tokens", None),
|
reasoning_tokens=getattr(output_tokens_details, "reasoning_tokens", None),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,14 @@
|
||||||
# litellm/proxy/vector_stores/vector_store_registry.py
|
# litellm/proxy/vector_stores/vector_store_registry.py
|
||||||
import json
|
import json
|
||||||
|
from collections.abc import Mapping
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import TYPE_CHECKING, Any, Final, get_args
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Any, # noqa: TID251 # untyped non_default_params dict is the only source of the unknown key type
|
||||||
|
Final,
|
||||||
|
cast, # noqa: TID251 # untyped non_default_params dict is the only source of the unknown key type
|
||||||
|
get_args,
|
||||||
|
)
|
||||||
|
|
||||||
from litellm._logging import verbose_logger
|
from litellm._logging import verbose_logger
|
||||||
from litellm.litellm_core_utils.core_helpers import remove_items_at_indices
|
from litellm.litellm_core_utils.core_helpers import remove_items_at_indices
|
||||||
|
|
@ -336,7 +343,9 @@ class VectorStoreRegistry:
|
||||||
try:
|
try:
|
||||||
# Check if it still exists in database
|
# Check if it still exists in database
|
||||||
db_vector_store = await ManagedVectorStoresRepository(prisma_client).table.find_unique(
|
db_vector_store = await ManagedVectorStoresRepository(prisma_client).table.find_unique(
|
||||||
where={"vector_store_id": vector_store_id}
|
where=cast( # cast-ok: every value is already an object, only the popped id is stub-untyped
|
||||||
|
"Mapping[str, object]", {"vector_store_id": vector_store_id}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if db_vector_store is None:
|
if db_vector_store is None:
|
||||||
# Vector store was deleted from database, remove from cache
|
# Vector store was deleted from database, remove from cache
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"ANN001": {
|
"ANN001": {
|
||||||
"limit": 3016
|
"limit": 3014
|
||||||
},
|
},
|
||||||
"ANN002": {
|
"ANN002": {
|
||||||
"limit": 71
|
"limit": 71
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
"limit": 827
|
"limit": 827
|
||||||
},
|
},
|
||||||
"ANN201": {
|
"ANN201": {
|
||||||
"limit": 2015
|
"limit": 2012
|
||||||
},
|
},
|
||||||
"ANN202": {
|
"ANN202": {
|
||||||
"limit": 852
|
"limit": 852
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
"limit": 133
|
"limit": 133
|
||||||
},
|
},
|
||||||
"ANN401": {
|
"ANN401": {
|
||||||
"limit": 1188
|
"limit": 1157
|
||||||
},
|
},
|
||||||
"ASYNC230": {
|
"ASYNC230": {
|
||||||
"limit": 11
|
"limit": 11
|
||||||
|
|
@ -33,13 +33,13 @@
|
||||||
"limit": 2
|
"limit": 2
|
||||||
},
|
},
|
||||||
"B006": {
|
"B006": {
|
||||||
"limit": 177
|
"limit": 176
|
||||||
},
|
},
|
||||||
"B008": {
|
"B008": {
|
||||||
"limit": 503
|
"limit": 503
|
||||||
},
|
},
|
||||||
"B009": {
|
"B009": {
|
||||||
"limit": 59
|
"limit": 58
|
||||||
},
|
},
|
||||||
"B010": {
|
"B010": {
|
||||||
"limit": 190
|
"limit": 190
|
||||||
|
|
@ -231,7 +231,7 @@
|
||||||
"limit": 5
|
"limit": 5
|
||||||
},
|
},
|
||||||
"TID251": {
|
"TID251": {
|
||||||
"limit": 1212
|
"limit": 1201
|
||||||
},
|
},
|
||||||
"TRY002": {
|
"TRY002": {
|
||||||
"limit": 524
|
"limit": 524
|
||||||
|
|
|
||||||
|
|
@ -416,6 +416,34 @@ async def test_update_returns_404_when_not_found():
|
||||||
assert exc_info.value.status_code == 404
|
assert exc_info.value.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_returns_404_when_row_deleted_before_write():
|
||||||
|
"""A mapping deleted between the read and the write must 404, not 500.
|
||||||
|
|
||||||
|
Prisma's update returns None when the row is gone, and the endpoint used to
|
||||||
|
dereference it for the cache key.
|
||||||
|
"""
|
||||||
|
from litellm.proxy._types import UpdateJWTKeyMappingRequest
|
||||||
|
|
||||||
|
mock_prisma = _mock_prisma()
|
||||||
|
mock_prisma.db.litellm_jwtkeymapping.find_unique.return_value = _mock_mapping()
|
||||||
|
mock_prisma.db.litellm_jwtkeymapping.update.return_value = None
|
||||||
|
mock_cache = AsyncMock()
|
||||||
|
|
||||||
|
data = UpdateJWTKeyMappingRequest(id="mapping-1", description="test")
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("litellm.proxy.proxy_server.prisma_client", mock_prisma), # test-quality-ok: proxy_server module global is the endpoint's only injection point
|
||||||
|
patch("litellm.proxy.proxy_server.user_api_key_cache", mock_cache), # test-quality-ok: proxy_server module global is the endpoint's only injection point
|
||||||
|
):
|
||||||
|
with pytest.raises(HTTPException) as exc_info:
|
||||||
|
await update_jwt_key_mapping(
|
||||||
|
data=data, user_api_key_dict=_make_admin_auth()
|
||||||
|
)
|
||||||
|
assert exc_info.value.status_code == 404
|
||||||
|
assert exc_info.value.detail == "Mapping not found"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_info_returns_404_when_not_found():
|
async def test_info_returns_404_when_not_found():
|
||||||
"""Getting info for non-existent mapping should return 404."""
|
"""Getting info for non-existent mapping should return 404."""
|
||||||
|
|
|
||||||
|
|
@ -428,3 +428,60 @@ async def test_migrate_legacy_grant_ids_no_ops_without_config_agents():
|
||||||
|
|
||||||
assert await registry.migrate_legacy_grant_ids(table=table) == GrantMigrationResult(rewritten=0, missed=0)
|
assert await registry.migrate_legacy_grant_ids(table=table) == GrantMigrationResult(rewritten=0, missed=0)
|
||||||
table.find_many.assert_not_awaited()
|
table.find_many.assert_not_awaited()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_agent_in_db_raises_when_row_deleted_mid_update():
|
||||||
|
"""Prisma's update returns None when the row vanished between read and write. Without a
|
||||||
|
guard the code dereferences None and reports an opaque AttributeError instead of the id."""
|
||||||
|
registry: Final = AgentRegistry()
|
||||||
|
mock_prisma: Final = MagicMock()
|
||||||
|
mock_prisma.db.litellm_agentstable.update = AsyncMock(return_value=None)
|
||||||
|
|
||||||
|
with pytest.raises(Exception, match="Error updating agent in DB") as exc_info:
|
||||||
|
await registry.update_agent_in_db(
|
||||||
|
agent_id="agent-123",
|
||||||
|
agent={
|
||||||
|
"agent_name": "Updated Agent",
|
||||||
|
"agent_card_params": _sample_agent_card_params(),
|
||||||
|
"litellm_params": {},
|
||||||
|
},
|
||||||
|
prisma_client=mock_prisma,
|
||||||
|
updated_by="test-user",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert str(exc_info.value) == "Error updating agent in DB: Agent not found, passed agent_id=agent-123"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_patch_agent_in_db_raises_when_row_deleted_mid_update():
|
||||||
|
"""Same race on PATCH: the existing row is read, then deleted before the update lands."""
|
||||||
|
registry: Final = AgentRegistry()
|
||||||
|
mock_prisma: Final = MagicMock()
|
||||||
|
mock_prisma.db.litellm_agentstable.find_unique = AsyncMock(
|
||||||
|
return_value={"agent_id": "agent-123", "agent_name": "Old Agent", "object_permission_id": None}
|
||||||
|
)
|
||||||
|
mock_prisma.db.litellm_agentstable.update = AsyncMock(return_value=None)
|
||||||
|
|
||||||
|
with pytest.raises(Exception, match="Error patching agent in DB") as exc_info:
|
||||||
|
await registry.patch_agent_in_db(
|
||||||
|
agent_id="agent-123",
|
||||||
|
agent={"agent_name": "Patched Agent"},
|
||||||
|
prisma_client=mock_prisma,
|
||||||
|
updated_by="test-user",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert str(exc_info.value) == "Error patching agent in DB: Agent not found, passed agent_id=agent-123"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_delete_agent_from_db_raises_when_row_already_gone():
|
||||||
|
"""Prisma's delete returns None for a missing row, which dict() cannot consume."""
|
||||||
|
registry: Final = AgentRegistry()
|
||||||
|
mock_prisma: Final = MagicMock()
|
||||||
|
mock_prisma.db.litellm_agentstable.delete = AsyncMock(return_value=None)
|
||||||
|
|
||||||
|
with pytest.raises(Exception, match="Error deleting agent from DB") as exc_info:
|
||||||
|
await registry.delete_agent_from_db(agent_id="agent-123", prisma_client=mock_prisma)
|
||||||
|
|
||||||
|
assert str(exc_info.value) == "Error deleting agent from DB: Agent not found, passed agent_id=agent-123"
|
||||||
|
|
|
||||||
|
|
@ -231,6 +231,30 @@ async def test_update_plugin_db_error_maps_to_structured_500():
|
||||||
assert "connection lost" in exc_info.value.detail["error"]
|
assert "connection lost" in exc_info.value.detail["error"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_plugin_deleted_mid_update_returns_404():
|
||||||
|
"""A concurrent delete between the find_unique pre-check and the update makes prisma's
|
||||||
|
update return None; that must surface the same 404 as a plain miss, not an AttributeError."""
|
||||||
|
name = "my-monorepo-plugin"
|
||||||
|
await register_plugin(
|
||||||
|
request=RegisterPluginRequest(name=name, source=_GIT_SUBDIR_SOURCE, version="1.0.0"),
|
||||||
|
user_api_key_dict=_USER,
|
||||||
|
)
|
||||||
|
|
||||||
|
table = litellm.proxy.proxy_server.prisma_client.db.litellm_claudecodeplugintable
|
||||||
|
table.update = AsyncMock(return_value=None)
|
||||||
|
|
||||||
|
with pytest.raises(HTTPException) as exc_info:
|
||||||
|
await update_plugin(
|
||||||
|
plugin_name=name,
|
||||||
|
request=UpdatePluginRequest(source={"source": "github", "repo": "org/replacement"}),
|
||||||
|
user_api_key_dict=_USER,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert exc_info.value.status_code == 404
|
||||||
|
assert exc_info.value.detail == {"error": f"Plugin '{name}' not found"}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_marketplace_skips_plugin_with_null_manifest():
|
async def test_get_marketplace_skips_plugin_with_null_manifest():
|
||||||
await register_plugin(
|
await register_plugin(
|
||||||
|
|
|
||||||
|
|
@ -695,7 +695,7 @@ def test_reset_budget_resets_endusers_with_null_budget_id(reset_budget_job, mock
|
||||||
"object_permission_id": None,
|
"object_permission_id": None,
|
||||||
"object_permission": None,
|
"object_permission": None,
|
||||||
"litellm_budget_table": None,
|
"litellm_budget_table": None,
|
||||||
"dict": lambda self=None: {
|
"model_dump": lambda self=None: {
|
||||||
"spend": 25.0,
|
"spend": 25.0,
|
||||||
"user_id": "enduser-implicit",
|
"user_id": "enduser-implicit",
|
||||||
"blocked": False,
|
"blocked": False,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,11 @@ from unittest.mock import AsyncMock, MagicMock
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
from litellm.proxy._experimental.mcp_server.db import get_mcp_servers_by_team
|
from litellm.proxy._experimental.mcp_server.db import (
|
||||||
|
approve_mcp_server,
|
||||||
|
get_mcp_servers_by_team,
|
||||||
|
reject_mcp_server,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _prisma_client_returning(team_record: object) -> MagicMock:
|
def _prisma_client_returning(team_record: object) -> MagicMock:
|
||||||
|
|
@ -38,3 +42,30 @@ async def test_fetch_mcp_servers_by_team(team_record, expected):
|
||||||
where={"team_id": "team-123"},
|
where={"team_id": "team-123"},
|
||||||
include={"object_permission": True},
|
include={"object_permission": True},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _prisma_client_with_missing_mcp_server_row() -> MagicMock:
|
||||||
|
prisma_client = MagicMock()
|
||||||
|
prisma_client.db.litellm_mcpservertable.update = AsyncMock(return_value=None)
|
||||||
|
return prisma_client
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_approve_mcp_server_raises_value_error_when_row_missing():
|
||||||
|
prisma_client = _prisma_client_with_missing_mcp_server_row()
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match=r"^MCP server not found, passed server_id=server-gone$"):
|
||||||
|
await approve_mcp_server(prisma_client, "server-gone", touched_by="admin")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_reject_mcp_server_raises_value_error_when_row_missing():
|
||||||
|
prisma_client = _prisma_client_with_missing_mcp_server_row()
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match=r"^MCP server not found, passed server_id=server-gone$"):
|
||||||
|
await reject_mcp_server(
|
||||||
|
prisma_client,
|
||||||
|
"server-gone",
|
||||||
|
touched_by="admin",
|
||||||
|
review_notes="spam",
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
|
from unittest.mock import AsyncMock, MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from litellm.integrations.custom_guardrail import CustomGuardrail
|
from litellm.integrations.custom_guardrail import CustomGuardrail
|
||||||
from litellm.proxy.guardrails.guardrail_registry import (
|
from litellm.proxy.guardrails.guardrail_registry import (
|
||||||
get_guardrail_initializer_from_hooks,
|
get_guardrail_initializer_from_hooks,
|
||||||
|
GuardrailRegistry,
|
||||||
InMemoryGuardrailHandler,
|
InMemoryGuardrailHandler,
|
||||||
)
|
)
|
||||||
from litellm.types.guardrails import GuardrailEventHooks, Guardrail, LitellmParams
|
from litellm.types.guardrails import GuardrailEventHooks, Guardrail, LitellmParams
|
||||||
|
|
@ -657,3 +660,22 @@ class TestScanOnlyToolResultsInitRefusal:
|
||||||
"scan_only_tool_results": True,
|
"scan_only_tool_results": True,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_guardrail_in_db_raises_when_row_missing():
|
||||||
|
prisma_client = MagicMock()
|
||||||
|
prisma_client.db.litellm_guardrailstable.update = AsyncMock(return_value=None)
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
Exception,
|
||||||
|
match=r"^Error updating guardrail in DB: Guardrail not found, passed guardrail_id=missing-guardrail$",
|
||||||
|
):
|
||||||
|
await GuardrailRegistry().update_guardrail_in_db(
|
||||||
|
guardrail_id="missing-guardrail",
|
||||||
|
guardrail=Guardrail(
|
||||||
|
guardrail_name="missing-guardrail",
|
||||||
|
litellm_params=LitellmParams(guardrail="bedrock", mode="pre_call"),
|
||||||
|
),
|
||||||
|
prisma_client=prisma_client,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -5513,3 +5513,46 @@ async def test_handle_group_membership_changes_already_in_team_is_noop(mocker):
|
||||||
)
|
)
|
||||||
|
|
||||||
assert mock_team_member_add.await_count == 2
|
assert mock_team_member_add.await_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_patch_group_404s_when_team_deleted_mid_request(mocker):
|
||||||
|
"""A group deleted between the existence check and the write must 404.
|
||||||
|
|
||||||
|
Prisma returns None from both the update and the refresh reads once the row is
|
||||||
|
gone, and patch_group used to dereference that None while building the response.
|
||||||
|
"""
|
||||||
|
group_id = "team-gone"
|
||||||
|
|
||||||
|
snapshot_team = LiteLLM_TeamTable(
|
||||||
|
team_id=group_id,
|
||||||
|
team_alias="Group",
|
||||||
|
members_with_roles=[Member(user_id="zed", role="user")],
|
||||||
|
metadata={"externalId": "grp-ext"},
|
||||||
|
)
|
||||||
|
|
||||||
|
patch_ops = SCIMPatchOp(
|
||||||
|
schemas=["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
||||||
|
Operations=[SCIMPatchOperation(op="replace", path="displayName", value="Renamed")],
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_prisma_client = mocker.MagicMock()
|
||||||
|
mock_prisma_client.db = mocker.MagicMock()
|
||||||
|
mock_prisma_client.db.litellm_teamtable = mocker.MagicMock()
|
||||||
|
mock_prisma_client.db.litellm_teamtable.find_unique = AsyncMock(side_effect=[snapshot_team, None, None])
|
||||||
|
mock_prisma_client.db.litellm_teamtable.update = AsyncMock(return_value=None)
|
||||||
|
|
||||||
|
mocker.patch( # test-quality-ok: stubs the collaborator so the test pins the endpoint's own error contract
|
||||||
|
"litellm.proxy.management_endpoints.scim.scim_v2._get_prisma_client_or_raise_exception",
|
||||||
|
AsyncMock(return_value=mock_prisma_client),
|
||||||
|
)
|
||||||
|
mocker.patch( # test-quality-ok: stubs the collaborator so the test pins the endpoint's own error contract
|
||||||
|
"litellm.proxy.management_endpoints.scim.scim_v2._recompute_scim_member_roles",
|
||||||
|
AsyncMock(),
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ProxyException) as exc_info:
|
||||||
|
await patch_group(group_id=group_id, patch_ops=patch_ops)
|
||||||
|
|
||||||
|
assert exc_info.value.code == "404"
|
||||||
|
assert f"Group not found with ID: {group_id}" in exc_info.value.message
|
||||||
|
|
|
||||||
|
|
@ -2398,7 +2398,7 @@ async def test_delete_user_cleans_up_created_by_invitation_links(mocker):
|
||||||
mock_user_row.user_id = "admin-creator"
|
mock_user_row.user_id = "admin-creator"
|
||||||
mock_user_row.user_email = "admin@example.com"
|
mock_user_row.user_email = "admin@example.com"
|
||||||
mock_user_row.teams = []
|
mock_user_row.teams = []
|
||||||
mock_user_row.json.return_value = "{}"
|
mock_user_row.model_dump_json.return_value = "{}"
|
||||||
mock_user_row.model_dump.return_value = {
|
mock_user_row.model_dump.return_value = {
|
||||||
"user_id": "admin-creator",
|
"user_id": "admin-creator",
|
||||||
"user_email": "admin@example.com",
|
"user_email": "admin@example.com",
|
||||||
|
|
|
||||||
|
|
@ -7766,6 +7766,45 @@ async def test_validate_key_list_check_key_hash_not_found():
|
||||||
assert "Key Hash not found" in exc_info.value.message
|
assert "Key Hash not found" in exc_info.value.message
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_validate_key_list_check_key_hash_row_missing():
|
||||||
|
"""A key_hash with no row reaches the same 'Key Hash not found' 403 as a failed
|
||||||
|
lookup, instead of blowing up inside the ownership check on a None row."""
|
||||||
|
mock_prisma_client = AsyncMock()
|
||||||
|
mock_prisma_client.db.litellm_usertable.find_unique = AsyncMock(
|
||||||
|
return_value=LiteLLM_UserTable(
|
||||||
|
user_id="test-user",
|
||||||
|
user_email="test@example.com",
|
||||||
|
teams=[],
|
||||||
|
organization_memberships=[],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
mock_prisma_client.db.litellm_verificationtoken.find_unique = AsyncMock(
|
||||||
|
return_value=None
|
||||||
|
)
|
||||||
|
|
||||||
|
user_api_key_dict = UserAPIKeyAuth(
|
||||||
|
user_role=LitellmUserRoles.INTERNAL_USER,
|
||||||
|
user_id="test-user",
|
||||||
|
api_key="sk-caller",
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ProxyException) as exc_info:
|
||||||
|
await validate_key_list_check(
|
||||||
|
user_api_key_dict=user_api_key_dict,
|
||||||
|
user_id=None,
|
||||||
|
team_id=None,
|
||||||
|
organization_id=None,
|
||||||
|
key_alias=None,
|
||||||
|
key_hash="hash-of-a-deleted-key",
|
||||||
|
prisma_client=mock_prisma_client,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert exc_info.value.code == "403" or exc_info.value.code == 403
|
||||||
|
assert exc_info.value.param == "key_hash"
|
||||||
|
assert "Key Hash not found" in exc_info.value.message
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_validate_key_list_check_proxy_admin_viewer_skips_db_lookup():
|
async def test_validate_key_list_check_proxy_admin_viewer_skips_db_lookup():
|
||||||
"""proxy_admin_viewer takes the same unscoped read fast-path as proxy_admin, so no
|
"""proxy_admin_viewer takes the same unscoped read fast-path as proxy_admin, so no
|
||||||
|
|
|
||||||
|
|
@ -3312,6 +3312,61 @@ class TestPatchModelBlockedAuthGate:
|
||||||
mock_prisma.db.litellm_proxymodeltable.update.assert_awaited_once()
|
mock_prisma.db.litellm_proxymodeltable.update.assert_awaited_once()
|
||||||
|
|
||||||
|
|
||||||
|
class TestPatchModelRowDeletedBeforeWrite:
|
||||||
|
"""A row deleted between the read and the update makes prisma's `update`
|
||||||
|
return None. That must surface patch_model's own 404 not-found contract,
|
||||||
|
not a 500 from dereferencing the missing row."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_patch_model_404s_when_update_returns_none(self):
|
||||||
|
from litellm.proxy.management_endpoints.model_management_endpoints import (
|
||||||
|
patch_model,
|
||||||
|
)
|
||||||
|
from litellm.proxy.proxy_server import ProxyException
|
||||||
|
|
||||||
|
admin = UserAPIKeyAuth(user_id="admin", user_role=LitellmUserRoles.PROXY_ADMIN)
|
||||||
|
existing_row = MagicMock()
|
||||||
|
existing_row.litellm_params = {"model": "openai/gpt-4o-mini"}
|
||||||
|
existing_row.model_dump.return_value = {
|
||||||
|
"model_name": "gpt-4o-mini",
|
||||||
|
"litellm_params": existing_row.litellm_params,
|
||||||
|
"model_info": {"id": "m1"},
|
||||||
|
}
|
||||||
|
existing_row.model_dump_json.return_value = "{}"
|
||||||
|
|
||||||
|
mock_prisma = MagicMock()
|
||||||
|
mock_prisma.db.litellm_proxymodeltable.find_unique = AsyncMock(
|
||||||
|
return_value=existing_row
|
||||||
|
)
|
||||||
|
mock_prisma.db.litellm_proxymodeltable.update = AsyncMock(return_value=None)
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("litellm.proxy.proxy_server.prisma_client", mock_prisma), # test-quality-ok: proxy_server module global is the endpoint's only injection point
|
||||||
|
patch("litellm.proxy.proxy_server.llm_router", MagicMock(**{"get_model_ids.return_value": ["m1"]})), # test-quality-ok: proxy_server module global is the endpoint's only injection point
|
||||||
|
patch("litellm.proxy.proxy_server.store_model_in_db", True), # test-quality-ok: proxy_server module global is the endpoint's only injection point
|
||||||
|
patch("litellm.proxy.proxy_server.premium_user", True), # test-quality-ok: proxy_server module global is the endpoint's only injection point
|
||||||
|
patch( # test-quality-ok: stubs the auth gate so the test exercises the not-found branch under test
|
||||||
|
"litellm.proxy.management_endpoints.model_management_endpoints.ModelManagementAuthChecks.can_user_make_model_call",
|
||||||
|
new=AsyncMock(return_value=None),
|
||||||
|
),
|
||||||
|
patch( # test-quality-ok: stubs the cache write so the test observes only the DB result handling
|
||||||
|
"litellm.proxy.management_endpoints.model_management_endpoints.clear_cache",
|
||||||
|
new=AsyncMock(
|
||||||
|
return_value=ReconcileOutcome(still_desired=None, live_after=None)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
):
|
||||||
|
with pytest.raises(ProxyException) as exc_info:
|
||||||
|
await patch_model(
|
||||||
|
model_id="m1",
|
||||||
|
patch_data=updateDeployment(blocked=True),
|
||||||
|
user_api_key_dict=admin,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert exc_info.value.code == "404"
|
||||||
|
assert exc_info.value.message == "Model m1 not found on proxy."
|
||||||
|
|
||||||
|
|
||||||
class TestWriteSurfacesReloadDrop:
|
class TestWriteSurfacesReloadDrop:
|
||||||
"""A model-write endpoint may report success only if every row it wrote is, after the
|
"""A model-write endpoint may report success only if every row it wrote is, after the
|
||||||
reload it triggered, live in this pod's router or deliberately environment-inactive."""
|
reload it triggered, live in this pod's router or deliberately environment-inactive."""
|
||||||
|
|
|
||||||
|
|
@ -1037,3 +1037,29 @@ async def test_get_organization_daily_activity_non_admin_without_org_admin_role_
|
||||||
|
|
||||||
assert get_daily_activity_mock.call_args.kwargs["entity_id"] == []
|
assert get_daily_activity_mock.call_args.kwargs["entity_id"] == []
|
||||||
assert org_table_find_many.call_args.kwargs["where"] == {"organization_id": {"in": []}}
|
assert org_table_find_many.call_args.kwargs["where"] == {"organization_id": {"in": []}}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_find_member_if_email_missing_row_raises_documented_400():
|
||||||
|
"""A user_email lookup that matches nothing returns None instead of raising, so the
|
||||||
|
only failure the surrounding try/except models is never entered. Without an explicit
|
||||||
|
None guard the next line dereferences None and /organization/member_add answers with
|
||||||
|
an AttributeError-driven 500 rather than the documented 400.
|
||||||
|
"""
|
||||||
|
from litellm.proxy.management_endpoints.organization_endpoints import (
|
||||||
|
find_member_if_email,
|
||||||
|
)
|
||||||
|
|
||||||
|
prisma_client = AsyncMock()
|
||||||
|
prisma_client.db.litellm_usertable.find_unique = AsyncMock(return_value=None)
|
||||||
|
|
||||||
|
with pytest.raises(HTTPException) as exc_info:
|
||||||
|
await find_member_if_email("missing@example.com", prisma_client)
|
||||||
|
|
||||||
|
assert exc_info.value.status_code == 400
|
||||||
|
assert exc_info.value.detail == {
|
||||||
|
"error": (
|
||||||
|
"Unique user not found for user_email=missing@example.com. Potential duplicate OR "
|
||||||
|
"non-existent user_email in LiteLLM_UserTable. Use 'user_id' instead."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1169,6 +1169,44 @@ async def test_delete_team_callback_404s_for_unknown_team():
|
||||||
mock_prisma.db.litellm_teamtable.update.assert_not_called()
|
mock_prisma.db.litellm_teamtable.update.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_team_callbacks_rejects_team_deleted_before_write():
|
||||||
|
"""A team deleted between the existence check and the write must be rejected.
|
||||||
|
|
||||||
|
Prisma's update returns None for a row that is gone, and add_team_callbacks
|
||||||
|
used to hand that None to the cache refresh and report success with a null
|
||||||
|
body. The rejection reuses this endpoint's own missing-team contract, so a
|
||||||
|
caller sees the same 400 whether the team vanished before or after the read.
|
||||||
|
"""
|
||||||
|
mock_prisma = _patch_prisma(_team_row(team_id="team-1", metadata={}))
|
||||||
|
mock_prisma.db.litellm_teamtable.update = AsyncMock(return_value=None)
|
||||||
|
|
||||||
|
data = AddTeamCallback(
|
||||||
|
callback_name="langfuse",
|
||||||
|
callback_type="success",
|
||||||
|
callback_vars={
|
||||||
|
"langfuse_public_key": "pk-demo",
|
||||||
|
"langfuse_secret_key": "sk-demo",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("litellm.proxy.proxy_server.prisma_client", mock_prisma), # test-quality-ok: proxy_server module global is the endpoint's only injection point
|
||||||
|
patch("litellm.proxy.proxy_server.master_key", None), # test-quality-ok: proxy_server module global is the endpoint's only injection point
|
||||||
|
):
|
||||||
|
with pytest.raises(HTTPException) as exc:
|
||||||
|
await add_team_callbacks(
|
||||||
|
data=data,
|
||||||
|
http_request=MagicMock(spec=Request),
|
||||||
|
team_id="team-1",
|
||||||
|
user_api_key_dict=_admin_auth(),
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_prisma.db.litellm_teamtable.update.assert_called_once()
|
||||||
|
assert exc.value.status_code == 400
|
||||||
|
assert exc.value.detail == {"error": "Team id = team-1 does not exist. Please use a different team id."}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_delete_team_callback_keeps_last_removal_from_reviving_legacy_shape():
|
async def test_delete_team_callback_keeps_last_removal_from_reviving_legacy_shape():
|
||||||
"""Removing the last entry must leave metadata["logging"] present and empty.
|
"""Removing the last entry must leave metadata["logging"] present and empty.
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import json
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from typing import Optional, cast
|
from typing import Final, Optional, cast
|
||||||
from unittest.mock import AsyncMock, MagicMock, PropertyMock, call, patch
|
from unittest.mock import AsyncMock, MagicMock, PropertyMock, call, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
@ -2204,6 +2204,100 @@ async def test_team_model_add_delete_refresh_team_cache(endpoint_name):
|
||||||
assert update_call_kwargs.get("include", {}).get("object_permission") is True
|
assert update_call_kwargs.get("include", {}).get("object_permission") is True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"endpoint_name",
|
||||||
|
["team_model_add", "team_model_delete", "update_team_member_permissions"],
|
||||||
|
)
|
||||||
|
async def test_team_write_404s_when_row_vanishes_before_update(endpoint_name):
|
||||||
|
"""A team deleted between the read and the write must 404.
|
||||||
|
|
||||||
|
Prisma's `update` returns None when no row matches `where`, and the team
|
||||||
|
row can be deleted between the read these endpoints do first and the
|
||||||
|
update that follows it. Without the guard, `team_model_add` /
|
||||||
|
`team_model_delete` hand that None to `_refresh_cached_team` (which
|
||||||
|
reads `team_row.team_id`) and `/team/permissions_update` returns None
|
||||||
|
out of a route declared to return a team, so a plain race turns into a
|
||||||
|
500 instead of the 404 every other not-found path in this file raises.
|
||||||
|
"""
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||||
|
|
||||||
|
from fastapi import Request
|
||||||
|
|
||||||
|
from litellm.proxy._types import (
|
||||||
|
LitellmUserRoles,
|
||||||
|
TeamModelAddRequest,
|
||||||
|
TeamModelDeleteRequest,
|
||||||
|
UserAPIKeyAuth,
|
||||||
|
)
|
||||||
|
from litellm.proxy.management_endpoints.team_endpoints import (
|
||||||
|
team_model_add,
|
||||||
|
team_model_delete,
|
||||||
|
update_team_member_permissions,
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_request = Mock(spec=Request)
|
||||||
|
mock_user_api_key_dict = UserAPIKeyAuth(
|
||||||
|
user_role=LitellmUserRoles.PROXY_ADMIN, user_id="test_user_id"
|
||||||
|
)
|
||||||
|
|
||||||
|
existing_team = MagicMock()
|
||||||
|
existing_team.team_id = "team-1234"
|
||||||
|
existing_team.model_dump.return_value = {
|
||||||
|
"team_id": "team-1234",
|
||||||
|
"models": ["bedrock-claude-sonnet-4", "openai/*"],
|
||||||
|
"team_member_permissions": [],
|
||||||
|
"spend": 0.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
call_endpoint_under_test: Final = {
|
||||||
|
"team_model_add": lambda: team_model_add(
|
||||||
|
data=TeamModelAddRequest(team_id="team-1234", models=["team-byok-1"]),
|
||||||
|
http_request=mock_request,
|
||||||
|
user_api_key_dict=mock_user_api_key_dict,
|
||||||
|
),
|
||||||
|
"team_model_delete": lambda: team_model_delete(
|
||||||
|
data=TeamModelDeleteRequest(team_id="team-1234", models=["openai/*"]),
|
||||||
|
http_request=mock_request,
|
||||||
|
user_api_key_dict=mock_user_api_key_dict,
|
||||||
|
),
|
||||||
|
"update_team_member_permissions": lambda: update_team_member_permissions(
|
||||||
|
data=UpdateTeamMemberPermissionsRequest(
|
||||||
|
team_id="team-1234",
|
||||||
|
team_member_permissions=["/key/generate"],
|
||||||
|
),
|
||||||
|
http_request=mock_request,
|
||||||
|
user_api_key_dict=mock_user_api_key_dict,
|
||||||
|
),
|
||||||
|
}[endpoint_name]
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("litellm.proxy.proxy_server.prisma_client") as mock_prisma_client, # test-quality-ok: proxy_server module global is the endpoint's only injection point
|
||||||
|
patch("litellm.proxy.proxy_server.user_api_key_cache"), # test-quality-ok: proxy_server module global is the endpoint's only injection point
|
||||||
|
patch("litellm.proxy.proxy_server.proxy_logging_obj"), # test-quality-ok: proxy_server module global is the endpoint's only injection point
|
||||||
|
patch( # test-quality-ok: stubs the cache write so the test observes only the DB result handling
|
||||||
|
"litellm.proxy.management_endpoints.team_endpoints._cache_team_object",
|
||||||
|
new_callable=AsyncMock,
|
||||||
|
),
|
||||||
|
patch( # test-quality-ok: stubs the collaborator so the test pins the endpoint's own error contract
|
||||||
|
"litellm.proxy.management_endpoints.team_endpoints.get_team_object",
|
||||||
|
new_callable=AsyncMock,
|
||||||
|
return_value=existing_team,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
mock_prisma_client.db.litellm_teamtable.find_unique = AsyncMock(
|
||||||
|
return_value=existing_team
|
||||||
|
)
|
||||||
|
mock_prisma_client.db.litellm_teamtable.update = AsyncMock(return_value=None)
|
||||||
|
mock_prisma_client.db.execute_raw = AsyncMock(return_value=None)
|
||||||
|
|
||||||
|
with pytest.raises(HTTPException) as exc_info:
|
||||||
|
await call_endpoint_under_test()
|
||||||
|
|
||||||
|
assert exc_info.value.status_code == 404
|
||||||
|
assert exc_info.value.detail == {"error": "Team not found, passed team_id=team-1234"}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_update_team_team_member_budget_not_passed_to_db(
|
async def test_update_team_team_member_budget_not_passed_to_db(
|
||||||
disable_audit_logging_for_mocked_team,
|
disable_audit_logging_for_mocked_team,
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ We patch the endpoint module's `_require_prisma` helper so we never need the
|
||||||
real proxy_server import chain (which pulls heavy optional deps).
|
real proxy_server import chain (which pulls heavy optional deps).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
@ -176,14 +177,36 @@ class _InMemoryTeamTable:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _make_team(team_id: str, *, admin_user_ids: List[str]) -> MagicMock:
|
def _make_team(team_id: str, *, admin_user_ids: List[str]) -> Any:
|
||||||
"""Build a team-row stub with `members_with_roles` shaped like Prisma."""
|
"""Build a real Prisma team row.
|
||||||
members = [MagicMock(user_id=uid, role="admin") for uid in admin_user_ids]
|
|
||||||
team = MagicMock()
|
`members_with_roles` is a JSON column, so Prisma deserializes it into plain
|
||||||
team.team_id = team_id
|
dicts, not `Member` objects. A stub that hands back attribute-style members
|
||||||
team.organization_id = None # skip org-admin path in tests
|
would let the router read `member.role` off something Prisma never returns.
|
||||||
team.members_with_roles = members
|
"""
|
||||||
return team
|
from prisma import models as prisma_models
|
||||||
|
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
return prisma_models.LiteLLM_TeamTable(
|
||||||
|
team_id=team_id,
|
||||||
|
organization_id=None,
|
||||||
|
members_with_roles=json.dumps([{"user_id": uid, "role": "admin"} for uid in admin_user_ids]),
|
||||||
|
metadata="{}",
|
||||||
|
models=[],
|
||||||
|
blocked=False,
|
||||||
|
created_at=now,
|
||||||
|
updated_at=now,
|
||||||
|
spend=0.0,
|
||||||
|
model_spend="{}",
|
||||||
|
model_max_budget="{}",
|
||||||
|
admins=[],
|
||||||
|
members=[],
|
||||||
|
team_member_permissions=[],
|
||||||
|
access_group_ids=[],
|
||||||
|
policies=[],
|
||||||
|
default_team_member_models=[],
|
||||||
|
allow_team_guardrail_config=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _make_prisma() -> MagicMock:
|
def _make_prisma() -> MagicMock:
|
||||||
|
|
@ -653,6 +676,39 @@ class TestMemoryEndpoints:
|
||||||
assert resp.json()["value"] == "new"
|
assert resp.json()["value"] == "new"
|
||||||
assert len(table.rows) == 1
|
assert len(table.rows) == 1
|
||||||
|
|
||||||
|
def test_put_memory_row_deleted_mid_update_returns_404(self):
|
||||||
|
"""
|
||||||
|
A concurrent DELETE landing between the visibility read and the write
|
||||||
|
makes Prisma's `update` return None. That must surface the same 404 the
|
||||||
|
read path uses, not an AttributeError bubbling out as an unhandled 500.
|
||||||
|
"""
|
||||||
|
table = self.prisma.db.litellm_memorytable
|
||||||
|
table.rows.append(
|
||||||
|
_make_row(
|
||||||
|
memory_id="m1",
|
||||||
|
key="notes",
|
||||||
|
value="old",
|
||||||
|
user_id="user-a",
|
||||||
|
team_id="team-a",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def vanished(*_args, **_kwargs):
|
||||||
|
return None
|
||||||
|
|
||||||
|
original_update = table.update
|
||||||
|
table.update = vanished
|
||||||
|
|
||||||
|
client = _make_client(_user_auth("user-a", "team-a"))
|
||||||
|
try:
|
||||||
|
with _patch_prisma(self.prisma):
|
||||||
|
resp = client.put("/v1/memory/notes", json={"value": "new"})
|
||||||
|
finally:
|
||||||
|
table.update = original_update
|
||||||
|
|
||||||
|
assert resp.status_code == 404, resp.text
|
||||||
|
assert resp.json()["detail"] == "Memory with key 'notes' not found"
|
||||||
|
|
||||||
def test_put_memory_explicit_null_metadata_clears_field(self):
|
def test_put_memory_explicit_null_metadata_clears_field(self):
|
||||||
"""
|
"""
|
||||||
prisma-client-python can't write a true SQL NULL to a `Json?` column
|
prisma-client-python can't write a true SQL NULL to a `Json?` column
|
||||||
|
|
@ -919,6 +975,28 @@ class TestMemoryEndpoints:
|
||||||
resp = client.delete("/v1/memory/notes")
|
resp = client.delete("/v1/memory/notes")
|
||||||
assert resp.status_code == 404
|
assert resp.status_code == 404
|
||||||
|
|
||||||
|
def test_delete_memory_row_deleted_mid_delete_returns_404(self):
|
||||||
|
table = self.prisma.db.litellm_memorytable
|
||||||
|
table.rows.append(
|
||||||
|
_make_row(memory_id="m1", key="notes", user_id="user-a", team_id="team-a")
|
||||||
|
)
|
||||||
|
|
||||||
|
async def vanished(*_args, **_kwargs):
|
||||||
|
return None
|
||||||
|
|
||||||
|
original_delete = table.delete
|
||||||
|
table.delete = vanished
|
||||||
|
|
||||||
|
client = _make_client(_user_auth("user-a", "team-a"))
|
||||||
|
try:
|
||||||
|
with _patch_prisma(self.prisma):
|
||||||
|
resp = client.delete("/v1/memory/notes")
|
||||||
|
finally:
|
||||||
|
table.delete = original_delete
|
||||||
|
|
||||||
|
assert resp.status_code == 404, resp.text
|
||||||
|
assert resp.json()["detail"] == "Memory with key 'notes' not found"
|
||||||
|
|
||||||
def test_visibility_filter_unscoped_for_admin_viewer(self):
|
def test_visibility_filter_unscoped_for_admin_viewer(self):
|
||||||
"""
|
"""
|
||||||
proxy_admin_viewer reads with the same unscoped filter as proxy_admin;
|
proxy_admin_viewer reads with the same unscoped filter as proxy_admin;
|
||||||
|
|
|
||||||
|
|
@ -191,3 +191,58 @@ async def test_get_prompt_info_by_base_id():
|
||||||
response.prompt_spec.prompt_id == "test_prompt"
|
response.prompt_spec.prompt_id == "test_prompt"
|
||||||
) # Should return base ID in spec response
|
) # Should return base ID in spec response
|
||||||
assert response.prompt_spec.version == 3 # Should identify it as version 3
|
assert response.prompt_spec.version == 3 # Should identify it as version 3
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_patch_prompt_row_deleted_mid_update_returns_404():
|
||||||
|
"""
|
||||||
|
A concurrent delete between the version lookup and the write makes Prisma's
|
||||||
|
`update` return None. That must reuse the endpoint's existing not-found 404
|
||||||
|
contract rather than blowing up into an opaque 500.
|
||||||
|
"""
|
||||||
|
from fastapi import HTTPException
|
||||||
|
|
||||||
|
from litellm.proxy.prompts.prompt_endpoints import PatchPromptRequest, patch_prompt
|
||||||
|
|
||||||
|
mock_user_auth = UserAPIKeyAuth(
|
||||||
|
api_key="sk-1234", user_role=LitellmUserRoles.PROXY_ADMIN
|
||||||
|
)
|
||||||
|
|
||||||
|
target_row = MagicMock()
|
||||||
|
target_row.id = "row-1"
|
||||||
|
target_row.version = 1
|
||||||
|
|
||||||
|
mock_prisma_client = MagicMock()
|
||||||
|
mock_prisma_client.db.litellm_prompttable.find_many = AsyncMock(
|
||||||
|
return_value=[target_row]
|
||||||
|
)
|
||||||
|
mock_prisma_client.db.litellm_prompttable.update = AsyncMock(return_value=None)
|
||||||
|
|
||||||
|
existing_prompt = PromptSpec(
|
||||||
|
prompt_id="test_prompt.v1",
|
||||||
|
litellm_params=PromptLiteLLMParams(
|
||||||
|
prompt_id="test_prompt", prompt_integration="dotprompt"
|
||||||
|
),
|
||||||
|
prompt_info=PromptInfo(prompt_type="db"),
|
||||||
|
)
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client), # test-quality-ok: proxy_server module global is the endpoint's only injection point
|
||||||
|
patch( # test-quality-ok: stubs the collaborator so the test pins the endpoint's own error contract
|
||||||
|
"litellm.proxy.prompts.prompt_registry.IN_MEMORY_PROMPT_REGISTRY"
|
||||||
|
) as mock_registry,
|
||||||
|
):
|
||||||
|
mock_registry.get_prompt_by_id.return_value = existing_prompt
|
||||||
|
|
||||||
|
with pytest.raises(HTTPException) as exc_info:
|
||||||
|
await patch_prompt(
|
||||||
|
prompt_id="test_prompt",
|
||||||
|
request=PatchPromptRequest(prompt_info=PromptInfo(prompt_type="db")),
|
||||||
|
user_api_key_dict=mock_user_auth,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert exc_info.value.status_code == 404
|
||||||
|
assert (
|
||||||
|
exc_info.value.detail
|
||||||
|
== "Prompt with ID test_prompt not found in environment development"
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1860,6 +1860,38 @@ def test_add_team_models_to_all_models_excludes_other_teams_byok_with_shared_nam
|
||||||
assert result == {"model-a-id": {"team-a"}}
|
assert result == {"model-a-id": {"team-a"}}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_non_admin_all_models_returns_user_models_when_user_row_missing():
|
||||||
|
"""
|
||||||
|
Regression test: /key/generate mints keys without a LiteLLM_UserTable row, so
|
||||||
|
find_unique returns None for such a user. That miss must neither raise (a 400
|
||||||
|
here, or the AttributeError on `user_row.teams` that used to surface as a 500)
|
||||||
|
nor leak team models: the user belongs to no team, so only the models they
|
||||||
|
added themselves come back.
|
||||||
|
"""
|
||||||
|
from litellm.proxy.proxy_server import non_admin_all_models
|
||||||
|
|
||||||
|
user_added_model = {"model_name": "my-model", "model_info": {"id": "user-model-1"}}
|
||||||
|
prisma_client = MagicMock()
|
||||||
|
prisma_client.db.litellm_usertable.find_unique = AsyncMock(return_value=None)
|
||||||
|
prisma_client.db.litellm_proxymodeltable.find_unique = AsyncMock(return_value=MagicMock(created_by="ghost-user"))
|
||||||
|
|
||||||
|
llm_router = MagicMock()
|
||||||
|
llm_router.get_model_list.return_value = [
|
||||||
|
user_added_model,
|
||||||
|
{"model_name": "team-model", "model_info": {"id": "team-model-1", "team_id": "team-a"}},
|
||||||
|
]
|
||||||
|
|
||||||
|
result = await non_admin_all_models(
|
||||||
|
all_models=[user_added_model],
|
||||||
|
llm_router=llm_router,
|
||||||
|
user_api_key_dict=UserAPIKeyAuth(api_key="sk-test", user_id="ghost-user"),
|
||||||
|
prisma_client=prisma_client,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result == [user_added_model]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_apply_search_filter_matches_team_public_model_name():
|
async def test_apply_search_filter_matches_team_public_model_name():
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -2770,7 +2770,7 @@ def mock_team_lookup(monkeypatch):
|
||||||
|
|
||||||
existing_team_ids: set = set()
|
existing_team_ids: set = set()
|
||||||
|
|
||||||
async def _find_many(where):
|
async def _find_many(where, **_):
|
||||||
requested = where["team_id"]["in"]
|
requested = where["team_id"]["in"]
|
||||||
return [{"team_id": team_id} for team_id in requested if team_id in existing_team_ids]
|
return [{"team_id": team_id} for team_id in requested if team_id in existing_team_ids]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ def test_jsonify_object_fallback_for_unserializable_dict(
|
||||||
|
|
||||||
|
|
||||||
def test_jsonify_object_error_on_non_dict(prisma_client: PrismaClient) -> None:
|
def test_jsonify_object_error_on_non_dict(prisma_client: PrismaClient) -> None:
|
||||||
with pytest.raises(AttributeError):
|
with pytest.raises(TypeError):
|
||||||
prisma_client.jsonify_object(None) # type: ignore[arg-type]
|
prisma_client.jsonify_object(None) # type: ignore[arg-type]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -134,7 +134,7 @@ def test_jsonify_team_object_converts_budget_limits_to_json_string(
|
||||||
|
|
||||||
|
|
||||||
def test_jsonify_team_object_error_on_non_dict(prisma_client: PrismaClient) -> None:
|
def test_jsonify_team_object_error_on_non_dict(prisma_client: PrismaClient) -> None:
|
||||||
with pytest.raises(AttributeError):
|
with pytest.raises(TypeError):
|
||||||
prisma_client.jsonify_team_object(None) # type: ignore[arg-type]
|
prisma_client.jsonify_team_object(None) # type: ignore[arg-type]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue