mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
Teams - Support default key expiry + UI - support enforcing access for members of specific SSO Group (#12023)
* fix(team_endpoints.py): support setting default key expiry allows admin to set key expiry on all team member keys makes it easier to setup default team for experimentation * feat(key_management_endpoints.py): allows admin to set duration for keys created by team members * feat(team_endpoints.py): support team_member_key_duration on `/team/update` allows setting max time team member keys are valid for * fix(team_info.tsx): ui component to update team member key duration * fix(team_info.tsx): support updating team member key duration, if set * feat(teams.tsx): add team member key duration param ui component allow admin to set this on UI * feat(ui_sso.py): support restricting ui access by sso group allows controlling who can/can't access the UI * feat(ssomodals.tsx): add initial commit adding sso group access to admin ui * feat(proxy_server.py): support reading + writing ui_access_mode from db allows admin to configure allowed sso groups from UI * feat(ui_sso.py): support enforcing all teams on sso jwt handler if ui access mode set via ui, support reading the value and enforcing it * feat(ui/): ui component for controlling sso access group allow admin to only allow users within specific sso group to log into UI * fix(uiaccesscontrolform.tsx): fix field names * feat(ui_sso.py): return received sso response in the clientside error message - enables easier debugging * test: add unit tests * fix: minor fixes
This commit is contained in:
parent
15dabf6573
commit
ac15ca3014
18 changed files with 864 additions and 85 deletions
|
|
@ -0,0 +1,30 @@
|
|||
from typing import Optional
|
||||
|
||||
from litellm.proxy._types import GenerateKeyRequest, LiteLLM_TeamTable
|
||||
|
||||
|
||||
def add_team_member_key_duration(
|
||||
team_table: Optional[LiteLLM_TeamTable],
|
||||
data: GenerateKeyRequest,
|
||||
) -> GenerateKeyRequest:
|
||||
if team_table is None:
|
||||
return data
|
||||
|
||||
if data.user_id is None: # only apply for team member keys, not service accounts
|
||||
return data
|
||||
|
||||
if (
|
||||
team_table.metadata is not None
|
||||
and team_table.metadata.get("team_member_key_duration") is not None
|
||||
):
|
||||
data.duration = team_table.metadata["team_member_key_duration"]
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def apply_enterprise_key_management_params(
|
||||
data: GenerateKeyRequest,
|
||||
team_table: Optional[LiteLLM_TeamTable],
|
||||
) -> GenerateKeyRequest:
|
||||
data = add_team_member_key_duration(team_table, data)
|
||||
return data
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
model_list:
|
||||
- model_name: gemini-2.5-pro
|
||||
litellm_params:
|
||||
model: gemini/gemini-2.5-pro
|
||||
model: gemini/gemini-2.5-pro
|
||||
|
|
|
|||
|
|
@ -1115,6 +1115,7 @@ class NewTeamRequest(TeamBase):
|
|||
team_member_budget: Optional[float] = (
|
||||
None # allow user to set a budget for all team members
|
||||
)
|
||||
team_member_key_duration: Optional[str] = None # e.g. "1d", "1w", "1m"
|
||||
|
||||
model_config = ConfigDict(protected_namespaces=())
|
||||
|
||||
|
|
@ -1157,6 +1158,7 @@ class UpdateTeamRequest(LiteLLMPydanticObjectBase):
|
|||
guardrails: Optional[List[str]] = None
|
||||
object_permission: Optional[LiteLLM_ObjectPermissionBase] = None
|
||||
team_member_budget: Optional[float] = None
|
||||
team_member_key_duration: Optional[str] = None
|
||||
|
||||
|
||||
class ResetTeamBudgetRequest(LiteLLMPydanticObjectBase):
|
||||
|
|
@ -2792,6 +2794,7 @@ LiteLLM_ManagementEndpoint_MetadataFields = [
|
|||
LiteLLM_ManagementEndpoint_MetadataFields_Premium = [
|
||||
"guardrails",
|
||||
"tags",
|
||||
"team_member_key_duration",
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -164,7 +164,9 @@ class JWTHandler:
|
|||
self.litellm_jwtauth.team_ids_jwt_field is not None
|
||||
and token.get(self.litellm_jwtauth.team_ids_jwt_field) is not None
|
||||
):
|
||||
|
||||
return token[self.litellm_jwtauth.team_ids_jwt_field]
|
||||
|
||||
return []
|
||||
|
||||
def get_end_user_id(
|
||||
|
|
|
|||
357
litellm/proxy/litellm.log
Normal file
357
litellm/proxy/litellm.log
Normal file
|
|
@ -0,0 +1,357 @@
|
|||
[92m18:10:09 - LiteLLM Router:INFO[0m: router.py:660 - Routing strategy: simple-shuffle
|
||||
[92m18:10:11 - LiteLLM Proxy:INFO[0m: utils.py:1317 - All necessary views exist!
|
||||
[92m18:10:11 - LiteLLM Router:WARNING[0m: router.py:4862 - Error upserting deployment: vertex_project, and vertex_location must be set in litellm_params for pass-through endpoints., ignoring and continuing with other deployments.
|
||||
[92m18:10:11 - LiteLLM Router:WARNING[0m: router.py:4862 - Error upserting deployment: vertex_project, and vertex_location must be set in litellm_params for pass-through endpoints., ignoring and continuing with other deployments.
|
||||
[92m18:10:23 - LiteLLM Proxy:INFO[0m: ui_sso.py:129 - Redirecting to SSO login for http://localhost:4000/sso/callback
|
||||
[92m18:10:27 - LiteLLM Proxy:INFO[0m: ui_sso.py:495 - Starting SSO callback
|
||||
[92m18:10:27 - LiteLLM Proxy:INFO[0m: ui_sso.py:550 - Redirecting to http://localhost:4000/sso/callback
|
||||
[92m18:10:28 - LiteLLM Proxy:INFO[0m: ui_sso.py:581 - SSO callback result: id='krrishd' email='krrishdholakia@gmail.com' first_name=None last_name=None display_name='a3f1c107-04dc-4c93-ae60-7f32eb4b05ce' picture=None provider=None team_ids=[]
|
||||
[92m18:10:28 - LiteLLM Proxy:INFO[0m: ui_sso.py:671 - user_defined_values for creating ui key: {'models': [], 'user_id': 'krrishd', 'user_email': 'krrishdholakia@gmail.com', 'max_budget': None, 'user_role': 'proxy_admin', 'budget_duration': None}
|
||||
[92m18:10:28 - LiteLLM Proxy:INFO[0m: utils.py:1856 - Data Inserted into Keys Table
|
||||
[92m18:10:28 - LiteLLM Proxy:INFO[0m: ui_sso.py:761 - user_id: krrishd; jwt_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoia3JyaXNoZCIsImtleSI6InNrLTVvOXVVc0ZaaTVBRFBiWERoanhCZlEiLCJ1c2VyX2VtYWlsIjoia3JyaXNoZGhvbGFraWFAZ21haWwuY29tIiwidXNlcl9yb2xlIjoicHJveHlfYWRtaW4iLCJsb2dpbl9tZXRob2QiOiJzc28iLCJwcmVtaXVtX3VzZXIiOnRydWUsImF1dGhfaGVhZGVyX25hbWUiOiJBdXRob3JpemF0aW9uIiwiZGlzYWJsZWRfbm9uX2FkbWluX3BlcnNvbmFsX2tleV9jcmVhdGlvbiI6ZmFsc2UsInNlcnZlcl9yb290X3BhdGgiOiIvIn0.OiZdFjZ2wiMhFbMCwu2cZYXh7oV5BB8Vta-Ysk5JBQU
|
||||
[92m18:10:28 - LiteLLM Proxy:INFO[0m: ui_sso.py:764 - Redirecting to http://localhost:4000/ui/?login=success
|
||||
[92m18:10:30 - LiteLLM Proxy:ERROR[0m: key_management_endpoints.py:2275 - Error in list_keys: Server disconnected without sending a response.
|
||||
Traceback (most recent call last):
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpx/_transports/default.py", line 101, in map_httpcore_exceptions
|
||||
yield
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpx/_transports/default.py", line 394, in handle_async_request
|
||||
resp = await self._pool.handle_async_request(req)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpcore/_async/connection_pool.py", line 216, in handle_async_request
|
||||
raise exc from None
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpcore/_async/connection_pool.py", line 196, in handle_async_request
|
||||
response = await connection.handle_async_request(
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpcore/_async/connection.py", line 101, in handle_async_request
|
||||
return await self._connection.handle_async_request(request)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpcore/_async/http11.py", line 143, in handle_async_request
|
||||
raise exc
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpcore/_async/http11.py", line 113, in handle_async_request
|
||||
) = await self._receive_response_headers(**kwargs)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpcore/_async/http11.py", line 186, in _receive_response_headers
|
||||
event = await self._receive_event(timeout=timeout)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpcore/_async/http11.py", line 238, in _receive_event
|
||||
raise RemoteProtocolError(msg)
|
||||
httpcore.RemoteProtocolError: Server disconnected without sending a response.
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
|
||||
Traceback (most recent call last):
|
||||
File "/Users/krrishdholakia/Documents/litellm/litellm/proxy/management_endpoints/key_management_endpoints.py", line 2255, in list_keys
|
||||
response = await _list_key_helper(
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Users/krrishdholakia/Documents/litellm/litellm/proxy/management_endpoints/key_management_endpoints.py", line 2434, in _list_key_helper
|
||||
total_count = await prisma_client.db.litellm_verificationtoken.count(
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/prisma/actions.py", line 10157, in count
|
||||
resp = await self._client._execute(
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/prisma/_base_client.py", line 543, in _execute
|
||||
return await self._engine.query(builder.build(), tx_id=self._tx_id)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/prisma/engine/_query.py", line 402, in query
|
||||
return await self.request(
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/prisma/engine/_http.py", line 217, in request
|
||||
response = await self.session.request(method, url, **kwargs)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/prisma/_async_http.py", line 26, in request
|
||||
return Response(await self.session.request(method, url, **kwargs))
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpx/_client.py", line 1540, in request
|
||||
return await self.send(request, auth=auth, follow_redirects=follow_redirects)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpx/_client.py", line 1629, in send
|
||||
response = await self._send_handling_auth(
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpx/_client.py", line 1657, in _send_handling_auth
|
||||
response = await self._send_handling_redirects(
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpx/_client.py", line 1694, in _send_handling_redirects
|
||||
response = await self._send_single_request(request)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpx/_client.py", line 1730, in _send_single_request
|
||||
response = await transport.handle_async_request(request)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpx/_transports/default.py", line 393, in handle_async_request
|
||||
with map_httpcore_exceptions():
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/contextlib.py", line 155, in __exit__
|
||||
self.gen.throw(typ, value, traceback)
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpx/_transports/default.py", line 118, in map_httpcore_exceptions
|
||||
raise mapped_exc(message) from exc
|
||||
httpx.RemoteProtocolError: Server disconnected without sending a response.
|
||||
[92m18:10:30 - LiteLLM Proxy:ERROR[0m: proxy_server.py:2730 - litellm.proxy_server.py::add_deployment() - Error getting new models from DB - All connection attempts failed
|
||||
Traceback (most recent call last):
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpx/_transports/default.py", line 101, in map_httpcore_exceptions
|
||||
yield
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpx/_transports/default.py", line 394, in handle_async_request
|
||||
resp = await self._pool.handle_async_request(req)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpcore/_async/connection_pool.py", line 216, in handle_async_request
|
||||
raise exc from None
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpcore/_async/connection_pool.py", line 196, in handle_async_request
|
||||
response = await connection.handle_async_request(
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpcore/_async/connection.py", line 99, in handle_async_request
|
||||
raise exc
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpcore/_async/connection.py", line 76, in handle_async_request
|
||||
stream = await self._connect(request)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpcore/_async/connection.py", line 122, in _connect
|
||||
stream = await self._network_backend.connect_tcp(**kwargs)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpcore/_backends/auto.py", line 30, in connect_tcp
|
||||
return await self._backend.connect_tcp(
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpcore/_backends/anyio.py", line 112, in connect_tcp
|
||||
with map_exceptions(exc_map):
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/contextlib.py", line 155, in __exit__
|
||||
self.gen.throw(typ, value, traceback)
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpcore/_exceptions.py", line 14, in map_exceptions
|
||||
raise to_exc(exc) from exc
|
||||
httpcore.ConnectError: All connection attempts failed
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
|
||||
Traceback (most recent call last):
|
||||
File "/Users/krrishdholakia/Documents/litellm/litellm/proxy/proxy_server.py", line 2728, in _get_models_from_db
|
||||
new_models = await prisma_client.db.litellm_proxymodeltable.find_many()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/prisma/actions.py", line 2540, in find_many
|
||||
resp = await self._client._execute(
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/prisma/_base_client.py", line 543, in _execute
|
||||
return await self._engine.query(builder.build(), tx_id=self._tx_id)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/prisma/engine/_query.py", line 402, in query
|
||||
return await self.request(
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/prisma/engine/_http.py", line 217, in request
|
||||
response = await self.session.request(method, url, **kwargs)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/prisma/_async_http.py", line 26, in request
|
||||
return Response(await self.session.request(method, url, **kwargs))
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpx/_client.py", line 1540, in request
|
||||
return await self.send(request, auth=auth, follow_redirects=follow_redirects)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpx/_client.py", line 1629, in send
|
||||
response = await self._send_handling_auth(
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpx/_client.py", line 1657, in _send_handling_auth
|
||||
response = await self._send_handling_redirects(
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpx/_client.py", line 1694, in _send_handling_redirects
|
||||
response = await self._send_single_request(request)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpx/_client.py", line 1730, in _send_single_request
|
||||
response = await transport.handle_async_request(request)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpx/_transports/default.py", line 393, in handle_async_request
|
||||
with map_httpcore_exceptions():
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/contextlib.py", line 155, in __exit__
|
||||
self.gen.throw(typ, value, traceback)
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpx/_transports/default.py", line 118, in map_httpcore_exceptions
|
||||
raise mapped_exc(message) from exc
|
||||
httpx.ConnectError: All connection attempts failed
|
||||
[92m18:10:30 - LiteLLM Proxy:ERROR[0m: utils.py:1404 - LiteLLM Prisma Client Exception get_generic_data: All connection attempts failed
|
||||
[92m18:10:30 - LiteLLM Proxy:ERROR[0m: utils.py:1404 - LiteLLM Prisma Client Exception get_generic_data: All connection attempts failed
|
||||
[92m18:10:30 - LiteLLM Proxy:ERROR[0m: proxy_server.py:2778 - litellm.proxy.proxy_server.py::ProxyConfig:add_deployment - All connection attempts failed
|
||||
Traceback (most recent call last):
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpx/_transports/default.py", line 101, in map_httpcore_exceptions
|
||||
yield
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpx/_transports/default.py", line 394, in handle_async_request
|
||||
resp = await self._pool.handle_async_request(req)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpcore/_async/connection_pool.py", line 216, in handle_async_request
|
||||
raise exc from None
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpcore/_async/connection_pool.py", line 196, in handle_async_request
|
||||
response = await connection.handle_async_request(
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpcore/_async/connection.py", line 99, in handle_async_request
|
||||
raise exc
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpcore/_async/connection.py", line 76, in handle_async_request
|
||||
stream = await self._connect(request)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpcore/_async/connection.py", line 122, in _connect
|
||||
stream = await self._network_backend.connect_tcp(**kwargs)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpcore/_backends/auto.py", line 30, in connect_tcp
|
||||
return await self._backend.connect_tcp(
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpcore/_backends/anyio.py", line 112, in connect_tcp
|
||||
with map_exceptions(exc_map):
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/contextlib.py", line 155, in __exit__
|
||||
self.gen.throw(typ, value, traceback)
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpcore/_exceptions.py", line 14, in map_exceptions
|
||||
raise to_exc(exc) from exc
|
||||
httpcore.ConnectError: All connection attempts failed
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
|
||||
Traceback (most recent call last):
|
||||
File "/Users/krrishdholakia/Documents/litellm/litellm/proxy/proxy_server.py", line 2760, in add_deployment
|
||||
await self._update_llm_router(
|
||||
File "/Users/krrishdholakia/Documents/litellm/litellm/proxy/proxy_server.py", line 2418, in _update_llm_router
|
||||
config_data = await proxy_config.get_config()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Users/krrishdholakia/Documents/litellm/litellm/proxy/proxy_server.py", line 1584, in get_config
|
||||
config = await self._update_config_from_db(
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Users/krrishdholakia/Documents/litellm/litellm/proxy/proxy_server.py", line 2706, in _update_config_from_db
|
||||
responses = await asyncio.gather(*_tasks)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Users/krrishdholakia/Documents/litellm/litellm/proxy/db/log_db_metrics.py", line 99, in wrapper
|
||||
raise e
|
||||
File "/Users/krrishdholakia/Documents/litellm/litellm/proxy/db/log_db_metrics.py", line 42, in wrapper
|
||||
result = await func(*args, **kwargs)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/backoff/_async.py", line 151, in retry
|
||||
ret = await target(*args, **kwargs)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Users/krrishdholakia/Documents/litellm/litellm/proxy/utils.py", line 1418, in get_generic_data
|
||||
raise e
|
||||
File "/Users/krrishdholakia/Documents/litellm/litellm/proxy/utils.py", line 1392, in get_generic_data
|
||||
response = await self.db.litellm_config.find_first( # type: ignore
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/prisma/actions.py", line 11822, in find_first
|
||||
resp = await self._client._execute(
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/prisma/_base_client.py", line 543, in _execute
|
||||
return await self._engine.query(builder.build(), tx_id=self._tx_id)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/prisma/engine/_query.py", line 402, in query
|
||||
return await self.request(
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/prisma/engine/_http.py", line 217, in request
|
||||
response = await self.session.request(method, url, **kwargs)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/prisma/_async_http.py", line 26, in request
|
||||
return Response(await self.session.request(method, url, **kwargs))
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpx/_client.py", line 1540, in request
|
||||
return await self.send(request, auth=auth, follow_redirects=follow_redirects)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpx/_client.py", line 1629, in send
|
||||
response = await self._send_handling_auth(
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpx/_client.py", line 1657, in _send_handling_auth
|
||||
response = await self._send_handling_redirects(
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpx/_client.py", line 1694, in _send_handling_redirects
|
||||
response = await self._send_single_request(request)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpx/_client.py", line 1730, in _send_single_request
|
||||
response = await transport.handle_async_request(request)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpx/_transports/default.py", line 393, in handle_async_request
|
||||
with map_httpcore_exceptions():
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/contextlib.py", line 155, in __exit__
|
||||
self.gen.throw(typ, value, traceback)
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpx/_transports/default.py", line 118, in map_httpcore_exceptions
|
||||
raise mapped_exc(message) from exc
|
||||
httpx.ConnectError: All connection attempts failed
|
||||
[92m18:10:30 - LiteLLM Proxy:ERROR[0m: utils.py:1404 - LiteLLM Prisma Client Exception get_generic_data: All connection attempts failed
|
||||
[92m18:10:30 - LiteLLM Proxy:ERROR[0m: utils.py:1404 - LiteLLM Prisma Client Exception get_generic_data: All connection attempts failed
|
||||
[92m18:10:30 - LiteLLM Proxy:INFO[0m: proxy_server.py:490 - Shutting down LiteLLM Proxy Server
|
||||
[92m18:11:47 - LiteLLM Router:INFO[0m: router.py:660 - Routing strategy: simple-shuffle
|
||||
[92m18:11:49 - LiteLLM Proxy:INFO[0m: utils.py:1317 - All necessary views exist!
|
||||
[92m18:11:50 - LiteLLM Router:WARNING[0m: router.py:4862 - Error upserting deployment: vertex_project, and vertex_location must be set in litellm_params for pass-through endpoints., ignoring and continuing with other deployments.
|
||||
[92m18:11:50 - LiteLLM Router:WARNING[0m: router.py:4862 - Error upserting deployment: vertex_project, and vertex_location must be set in litellm_params for pass-through endpoints., ignoring and continuing with other deployments.
|
||||
[92m18:12:00 - LiteLLM Proxy:ERROR[0m: proxy_server.py:2925 - litellm.proxy_server.py::get_credentials() - Error getting credentials from DB - Server disconnected without sending a response.
|
||||
Traceback (most recent call last):
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpx/_transports/default.py", line 101, in map_httpcore_exceptions
|
||||
yield
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpx/_transports/default.py", line 394, in handle_async_request
|
||||
resp = await self._pool.handle_async_request(req)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpcore/_async/connection_pool.py", line 216, in handle_async_request
|
||||
raise exc from None
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpcore/_async/connection_pool.py", line 196, in handle_async_request
|
||||
response = await connection.handle_async_request(
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpcore/_async/connection.py", line 101, in handle_async_request
|
||||
return await self._connection.handle_async_request(request)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpcore/_async/http11.py", line 143, in handle_async_request
|
||||
raise exc
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpcore/_async/http11.py", line 113, in handle_async_request
|
||||
) = await self._receive_response_headers(**kwargs)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpcore/_async/http11.py", line 186, in _receive_response_headers
|
||||
event = await self._receive_event(timeout=timeout)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpcore/_async/http11.py", line 238, in _receive_event
|
||||
raise RemoteProtocolError(msg)
|
||||
httpcore.RemoteProtocolError: Server disconnected without sending a response.
|
||||
|
||||
The above exception was the direct cause of the following exception:
|
||||
|
||||
Traceback (most recent call last):
|
||||
File "/Users/krrishdholakia/Documents/litellm/litellm/proxy/proxy_server.py", line 2916, in get_credentials
|
||||
credentials = await prisma_client.db.litellm_credentialstable.find_many()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/prisma/actions.py", line 1502, in find_many
|
||||
resp = await self._client._execute(
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/prisma/_base_client.py", line 543, in _execute
|
||||
return await self._engine.query(builder.build(), tx_id=self._tx_id)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/prisma/engine/_query.py", line 402, in query
|
||||
return await self.request(
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/prisma/engine/_http.py", line 217, in request
|
||||
response = await self.session.request(method, url, **kwargs)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/prisma/_async_http.py", line 26, in request
|
||||
return Response(await self.session.request(method, url, **kwargs))
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpx/_client.py", line 1540, in request
|
||||
return await self.send(request, auth=auth, follow_redirects=follow_redirects)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpx/_client.py", line 1629, in send
|
||||
response = await self._send_handling_auth(
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpx/_client.py", line 1657, in _send_handling_auth
|
||||
response = await self._send_handling_redirects(
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpx/_client.py", line 1694, in _send_handling_redirects
|
||||
response = await self._send_single_request(request)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpx/_client.py", line 1730, in _send_single_request
|
||||
response = await transport.handle_async_request(request)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpx/_transports/default.py", line 393, in handle_async_request
|
||||
with map_httpcore_exceptions():
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/contextlib.py", line 155, in __exit__
|
||||
self.gen.throw(typ, value, traceback)
|
||||
File "/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11/site-packages/httpx/_transports/default.py", line 118, in map_httpcore_exceptions
|
||||
raise mapped_exc(message) from exc
|
||||
httpx.RemoteProtocolError: Server disconnected without sending a response.
|
||||
[92m18:12:01 - LiteLLM Proxy:INFO[0m: proxy_server.py:490 - Shutting down LiteLLM Proxy Server
|
||||
[92m18:12:14 - LiteLLM Router:INFO[0m: router.py:660 - Routing strategy: simple-shuffle
|
||||
[92m18:12:16 - LiteLLM Proxy:INFO[0m: utils.py:1317 - All necessary views exist!
|
||||
[92m18:12:16 - LiteLLM Router:WARNING[0m: router.py:4862 - Error upserting deployment: vertex_project, and vertex_location must be set in litellm_params for pass-through endpoints., ignoring and continuing with other deployments.
|
||||
[92m18:12:16 - LiteLLM Router:WARNING[0m: router.py:4862 - Error upserting deployment: vertex_project, and vertex_location must be set in litellm_params for pass-through endpoints., ignoring and continuing with other deployments.
|
||||
[92m18:12:21 - LiteLLM Proxy:INFO[0m: ui_sso.py:129 - Redirecting to SSO login for http://localhost:4000/sso/callback
|
||||
[92m18:12:26 - LiteLLM Proxy:INFO[0m: ui_sso.py:495 - Starting SSO callback
|
||||
[92m18:12:26 - LiteLLM Proxy:INFO[0m: ui_sso.py:550 - Redirecting to http://localhost:4000/sso/callback
|
||||
[92m18:12:26 - LiteLLM Proxy:INFO[0m: ui_sso.py:581 - SSO callback result: id='krrishd' email='krrishdholakia@gmail.com' first_name=None last_name=None display_name='a3f1c107-04dc-4c93-ae60-7f32eb4b05ce' picture=None provider=None team_ids=[]
|
||||
[92m18:12:27 - LiteLLM Proxy:INFO[0m: ui_sso.py:672 - user_defined_values for creating ui key: {'models': [], 'user_id': 'krrishd', 'user_email': 'krrishdholakia@gmail.com', 'max_budget': None, 'user_role': 'proxy_admin', 'budget_duration': None}
|
||||
[92m18:12:27 - LiteLLM Proxy:INFO[0m: utils.py:1856 - Data Inserted into Keys Table
|
||||
[92m18:12:27 - LiteLLM Proxy:INFO[0m: ui_sso.py:762 - user_id: krrishd; jwt_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoia3JyaXNoZCIsImtleSI6InNrLUQzMEFpdW9lckU3YlMyakFXWVFLd1EiLCJ1c2VyX2VtYWlsIjoia3JyaXNoZGhvbGFraWFAZ21haWwuY29tIiwidXNlcl9yb2xlIjoicHJveHlfYWRtaW4iLCJsb2dpbl9tZXRob2QiOiJzc28iLCJwcmVtaXVtX3VzZXIiOnRydWUsImF1dGhfaGVhZGVyX25hbWUiOiJBdXRob3JpemF0aW9uIiwiZGlzYWJsZWRfbm9uX2FkbWluX3BlcnNvbmFsX2tleV9jcmVhdGlvbiI6ZmFsc2UsInNlcnZlcl9yb290X3BhdGgiOiIvIn0.EzYP86hw12J4WHLe6ZZz4YgVNGPnxM_PHqLjINH2_-U
|
||||
[92m18:12:27 - LiteLLM Proxy:INFO[0m: ui_sso.py:765 - Redirecting to http://localhost:4000/ui/?login=success
|
||||
[92m18:12:31 - LiteLLM Proxy:INFO[0m: proxy_server.py:490 - Shutting down LiteLLM Proxy Server
|
||||
[92m18:15:07 - LiteLLM Router:INFO[0m: router.py:660 - Routing strategy: simple-shuffle
|
||||
[92m18:15:09 - LiteLLM Proxy:INFO[0m: utils.py:1317 - All necessary views exist!
|
||||
[92m18:15:09 - LiteLLM Router:WARNING[0m: router.py:4862 - Error upserting deployment: vertex_project, and vertex_location must be set in litellm_params for pass-through endpoints., ignoring and continuing with other deployments.
|
||||
[92m18:15:09 - LiteLLM Router:WARNING[0m: router.py:4862 - Error upserting deployment: vertex_project, and vertex_location must be set in litellm_params for pass-through endpoints., ignoring and continuing with other deployments.
|
||||
[92m18:15:17 - LiteLLM Proxy:INFO[0m: utils.py:1916 - Data Inserted into Config Table
|
||||
[92m18:15:28 - LiteLLM Proxy:INFO[0m: ui_sso.py:129 - Redirecting to SSO login for http://localhost:4000/sso/callback
|
||||
[92m18:15:32 - LiteLLM Proxy:INFO[0m: ui_sso.py:495 - Starting SSO callback
|
||||
[92m18:15:32 - LiteLLM Proxy:INFO[0m: ui_sso.py:550 - Redirecting to http://localhost:4000/sso/callback
|
||||
[92m18:15:32 - LiteLLM Proxy:INFO[0m: ui_sso.py:581 - SSO callback result: id='krrishd' email='krrishdholakia@gmail.com' first_name=None last_name=None display_name='a3f1c107-04dc-4c93-ae60-7f32eb4b05ce' picture=None provider=None team_ids=[]
|
||||
[92m18:15:37 - LiteLLM Proxy:INFO[0m: proxy_server.py:490 - Shutting down LiteLLM Proxy Server
|
||||
|
|
@ -512,6 +512,20 @@ async def generate_key_fn( # noqa: PLR0915
|
|||
},
|
||||
)
|
||||
|
||||
# APPLY ENTERPRISE KEY MANAGEMENT PARAMS
|
||||
try:
|
||||
from litellm_enterprise.proxy.management_endpoints.key_management_endpoints import (
|
||||
apply_enterprise_key_management_params,
|
||||
)
|
||||
|
||||
data = apply_enterprise_key_management_params(data, team_table)
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.info(
|
||||
"litellm.proxy.proxy_server.generate_key_fn(): Enterprise key management params not applied - {}".format(
|
||||
str(e)
|
||||
)
|
||||
)
|
||||
|
||||
# TODO: @ishaan-jaff: Migrate all budget tracking to use LiteLLM_BudgetTable
|
||||
_budget_id = data.budget_id
|
||||
if prisma_client is not None and data.soft_budget is not None:
|
||||
|
|
@ -536,7 +550,7 @@ async def generate_key_fn( # noqa: PLR0915
|
|||
# ADD METADATA FIELDS
|
||||
# Set Management Endpoint Metadata Fields
|
||||
for field in LiteLLM_ManagementEndpoint_MetadataFields_Premium:
|
||||
if getattr(data, field) is not None:
|
||||
if getattr(data, field, None) is not None:
|
||||
_set_object_metadata_field(
|
||||
object_data=data,
|
||||
field_name=field,
|
||||
|
|
@ -589,9 +603,9 @@ async def generate_key_fn( # noqa: PLR0915
|
|||
request_type="key", **data_json, table_name="key"
|
||||
)
|
||||
|
||||
response[
|
||||
"soft_budget"
|
||||
] = data.soft_budget # include the user-input soft budget in the response
|
||||
response["soft_budget"] = (
|
||||
data.soft_budget
|
||||
) # include the user-input soft budget in the response
|
||||
|
||||
response = GenerateKeyResponse(**response)
|
||||
|
||||
|
|
@ -667,9 +681,9 @@ async def _set_object_permission(
|
|||
data=data_json["object_permission"],
|
||||
)
|
||||
)
|
||||
data_json[
|
||||
"object_permission_id"
|
||||
] = created_object_permission.object_permission_id
|
||||
data_json["object_permission_id"] = (
|
||||
created_object_permission.object_permission_id
|
||||
)
|
||||
|
||||
# delete the object_permission from the data_json
|
||||
data_json.pop("object_permission")
|
||||
|
|
@ -1652,10 +1666,10 @@ async def delete_verification_tokens(
|
|||
try:
|
||||
if prisma_client:
|
||||
tokens = [_hash_token_if_needed(token=key) for key in tokens]
|
||||
_keys_being_deleted: List[
|
||||
LiteLLM_VerificationToken
|
||||
] = await prisma_client.db.litellm_verificationtoken.find_many(
|
||||
where={"token": {"in": tokens}}
|
||||
_keys_being_deleted: List[LiteLLM_VerificationToken] = (
|
||||
await prisma_client.db.litellm_verificationtoken.find_many(
|
||||
where={"token": {"in": tokens}}
|
||||
)
|
||||
)
|
||||
|
||||
if len(_keys_being_deleted) == 0:
|
||||
|
|
@ -1763,9 +1777,9 @@ async def _rotate_master_key(
|
|||
from litellm.proxy.proxy_server import proxy_config
|
||||
|
||||
try:
|
||||
models: Optional[
|
||||
List
|
||||
] = await prisma_client.db.litellm_proxymodeltable.find_many()
|
||||
models: Optional[List] = (
|
||||
await prisma_client.db.litellm_proxymodeltable.find_many()
|
||||
)
|
||||
except Exception:
|
||||
models = None
|
||||
# 2. process model table
|
||||
|
|
@ -2057,11 +2071,11 @@ async def validate_key_list_check(
|
|||
param="user_id",
|
||||
code=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
complete_user_info_db_obj: Optional[
|
||||
BaseModel
|
||||
] = await prisma_client.db.litellm_usertable.find_unique(
|
||||
where={"user_id": user_api_key_dict.user_id},
|
||||
include={"organization_memberships": True},
|
||||
complete_user_info_db_obj: Optional[BaseModel] = (
|
||||
await prisma_client.db.litellm_usertable.find_unique(
|
||||
where={"user_id": user_api_key_dict.user_id},
|
||||
include={"organization_memberships": True},
|
||||
)
|
||||
)
|
||||
|
||||
if complete_user_info_db_obj is None:
|
||||
|
|
@ -2147,10 +2161,10 @@ async def get_admin_team_ids(
|
|||
if complete_user_info is None:
|
||||
return []
|
||||
# Get all teams that user is an admin of
|
||||
teams: Optional[
|
||||
List[BaseModel]
|
||||
] = await prisma_client.db.litellm_teamtable.find_many(
|
||||
where={"team_id": {"in": complete_user_info.teams}}
|
||||
teams: Optional[List[BaseModel]] = (
|
||||
await prisma_client.db.litellm_teamtable.find_many(
|
||||
where={"team_id": {"in": complete_user_info.teams}}
|
||||
)
|
||||
)
|
||||
if teams is None:
|
||||
return []
|
||||
|
|
@ -2403,12 +2417,14 @@ async def _list_key_helper(
|
|||
where=where, # type: ignore
|
||||
skip=skip, # type: ignore
|
||||
take=size, # type: ignore
|
||||
order=order_by
|
||||
if order_by
|
||||
else [
|
||||
{"created_at": "desc"},
|
||||
{"token": "desc"}, # fallback sort
|
||||
],
|
||||
order=(
|
||||
order_by
|
||||
if order_by
|
||||
else [
|
||||
{"created_at": "desc"},
|
||||
{"token": "desc"}, # fallback sort
|
||||
]
|
||||
),
|
||||
include={"object_permission": True},
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
from typing import Dict, Union
|
||||
|
||||
from litellm.proxy._types import LitellmUserRoles
|
||||
|
||||
|
||||
def check_is_admin_only_access(ui_access_mode: str) -> bool:
|
||||
def check_is_admin_only_access(ui_access_mode: Union[str, Dict]) -> bool:
|
||||
"""Checks ui access mode is admin_only"""
|
||||
return ui_access_mode == "admin_only"
|
||||
if isinstance(ui_access_mode, str):
|
||||
return ui_access_mode == "admin_only"
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def has_admin_ui_access(user_role: str) -> bool:
|
||||
|
|
|
|||
|
|
@ -262,6 +262,7 @@ async def new_team( # noqa: PLR0915
|
|||
- guardrails: Optional[List[str]] - Guardrails for the team. [Docs](https://docs.litellm.ai/docs/proxy/guardrails)
|
||||
- object_permission: Optional[LiteLLM_ObjectPermissionBase] - team-specific object permission. Example - {"vector_stores": ["vector_store_1", "vector_store_2"]}. IF null or {} then no object permission.
|
||||
- team_member_budget: Optional[float] - The maximum budget allocated to an individual team member.
|
||||
- team_member_key_duration: Optional[str] - The duration for a team member's key. e.g. "1d", "1w", "1mo"
|
||||
|
||||
Returns:
|
||||
- team_id: (str) Unique team id - used for tracking spend across multiple keys for same team id.
|
||||
|
|
@ -688,6 +689,7 @@ async def update_team(
|
|||
- guardrails: Optional[List[str]] - Guardrails for the team. [Docs](https://docs.litellm.ai/docs/proxy/guardrails)
|
||||
- object_permission: Optional[LiteLLM_ObjectPermissionBase] - team-specific object permission. Example - {"vector_stores": ["vector_store_1", "vector_store_2"]}. IF null or {} then no object permission.
|
||||
- team_member_budget: Optional[float] - The maximum budget allocated to an individual team member.
|
||||
- team_member_key_duration: Optional[str] - The duration for a team member's key. e.g. "1d", "1w", "1mo"
|
||||
Example - update team TPM Limit
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -145,7 +145,11 @@ async def google_login(request: Request): # noqa: PLR0915
|
|||
return HTMLResponse(content=html_form, status_code=200)
|
||||
|
||||
|
||||
def generic_response_convertor(response, jwt_handler: JWTHandler):
|
||||
def generic_response_convertor(
|
||||
response,
|
||||
jwt_handler: JWTHandler,
|
||||
sso_jwt_handler: Optional[JWTHandler] = None,
|
||||
):
|
||||
generic_user_id_attribute_name = os.getenv(
|
||||
"GENERIC_USER_ID_ATTRIBUTE", "preferred_username"
|
||||
)
|
||||
|
|
@ -171,6 +175,13 @@ def generic_response_convertor(response, jwt_handler: JWTHandler):
|
|||
f" generic_user_id_attribute_name: {generic_user_id_attribute_name}\n generic_user_email_attribute_name: {generic_user_email_attribute_name}"
|
||||
)
|
||||
|
||||
all_teams = []
|
||||
if sso_jwt_handler is not None:
|
||||
team_ids = sso_jwt_handler.get_team_ids_from_jwt(cast(dict, response))
|
||||
all_teams.extend(team_ids)
|
||||
|
||||
team_ids = jwt_handler.get_team_ids_from_jwt(cast(dict, response))
|
||||
all_teams.extend(team_ids)
|
||||
return CustomOpenID(
|
||||
id=response.get(generic_user_id_attribute_name),
|
||||
display_name=response.get(generic_user_display_name_attribute_name),
|
||||
|
|
@ -178,20 +189,24 @@ def generic_response_convertor(response, jwt_handler: JWTHandler):
|
|||
first_name=response.get(generic_user_first_name_attribute_name),
|
||||
last_name=response.get(generic_user_last_name_attribute_name),
|
||||
provider=response.get(generic_provider_attribute_name),
|
||||
team_ids=jwt_handler.get_team_ids_from_jwt(cast(dict, response)),
|
||||
team_ids=all_teams,
|
||||
)
|
||||
|
||||
|
||||
async def get_generic_sso_response(
|
||||
request: Request,
|
||||
jwt_handler: JWTHandler,
|
||||
sso_jwt_handler: Optional[
|
||||
JWTHandler
|
||||
], # sso specific jwt handler - used for restricted sso group access control
|
||||
generic_client_id: str,
|
||||
redirect_url: str,
|
||||
) -> Union[OpenID, dict]:
|
||||
) -> Tuple[Union[OpenID, dict], Optional[dict]]: # return received response
|
||||
# make generic sso provider
|
||||
from fastapi_sso.sso.base import DiscoveryDocument
|
||||
from fastapi_sso.sso.generic import create_provider
|
||||
|
||||
received_response: Optional[dict] = None
|
||||
generic_client_secret = os.getenv("GENERIC_CLIENT_SECRET", None)
|
||||
generic_scope = os.getenv("GENERIC_SCOPE", "openid email profile").split(" ")
|
||||
generic_authorization_endpoint = os.getenv("GENERIC_AUTHORIZATION_ENDPOINT", None)
|
||||
|
|
@ -242,9 +257,12 @@ async def get_generic_sso_response(
|
|||
)
|
||||
|
||||
def response_convertor(response, client):
|
||||
nonlocal received_response # return for user debugging
|
||||
received_response = response
|
||||
return generic_response_convertor(
|
||||
response=response,
|
||||
jwt_handler=jwt_handler,
|
||||
sso_jwt_handler=sso_jwt_handler,
|
||||
)
|
||||
|
||||
SSOProvider = create_provider(
|
||||
|
|
@ -284,7 +302,7 @@ async def get_generic_sso_response(
|
|||
)
|
||||
raise e
|
||||
verbose_proxy_logger.debug("generic result: %s", result)
|
||||
return result or {}
|
||||
return result or {}, received_response
|
||||
|
||||
|
||||
async def create_team_member_add_task(team_id, user_info):
|
||||
|
|
@ -480,6 +498,8 @@ async def check_and_update_if_proxy_admin_id(
|
|||
async def auth_callback(request: Request): # noqa: PLR0915
|
||||
"""Verify login"""
|
||||
verbose_proxy_logger.info("Starting SSO callback")
|
||||
from litellm.proxy._types import LiteLLM_JWTAuth
|
||||
from litellm.proxy.auth.handle_jwt import JWTHandler
|
||||
from litellm.proxy.management_endpoints.key_management_endpoints import (
|
||||
generate_key_helper_fn,
|
||||
)
|
||||
|
|
@ -490,7 +510,6 @@ async def auth_callback(request: Request): # noqa: PLR0915
|
|||
premium_user,
|
||||
prisma_client,
|
||||
proxy_logging_obj,
|
||||
ui_access_mode,
|
||||
user_api_key_cache,
|
||||
user_custom_sso,
|
||||
)
|
||||
|
|
@ -502,9 +521,25 @@ async def auth_callback(request: Request): # noqa: PLR0915
|
|||
status_code=500, detail=CommonProxyErrors.db_not_connected_error.value
|
||||
)
|
||||
|
||||
sso_jwt_handler: Optional[JWTHandler] = None
|
||||
ui_access_mode = general_settings.get("ui_access_mode", None)
|
||||
if ui_access_mode is not None and isinstance(ui_access_mode, dict):
|
||||
sso_jwt_handler = JWTHandler()
|
||||
sso_jwt_handler.update_environment(
|
||||
prisma_client=prisma_client,
|
||||
user_api_key_cache=user_api_key_cache,
|
||||
litellm_jwtauth=LiteLLM_JWTAuth(
|
||||
team_ids_jwt_field=general_settings.get("ui_access_mode", {}).get(
|
||||
"sso_group_jwt_field", None
|
||||
),
|
||||
),
|
||||
leeway=0,
|
||||
)
|
||||
|
||||
microsoft_client_id = os.getenv("MICROSOFT_CLIENT_ID", None)
|
||||
google_client_id = os.getenv("GOOGLE_CLIENT_ID", None)
|
||||
generic_client_id = os.getenv("GENERIC_CLIENT_ID", None)
|
||||
received_response: Optional[dict] = None
|
||||
# get url from request
|
||||
if master_key is None:
|
||||
raise ProxyException(
|
||||
|
|
@ -532,11 +567,12 @@ async def auth_callback(request: Request): # noqa: PLR0915
|
|||
redirect_url=redirect_url,
|
||||
)
|
||||
elif generic_client_id is not None:
|
||||
result = await get_generic_sso_response(
|
||||
result, received_response = await get_generic_sso_response(
|
||||
request=request,
|
||||
jwt_handler=jwt_handler,
|
||||
generic_client_id=generic_client_id,
|
||||
redirect_url=redirect_url,
|
||||
sso_jwt_handler=sso_jwt_handler,
|
||||
)
|
||||
|
||||
if result is None:
|
||||
|
|
@ -547,6 +583,7 @@ async def auth_callback(request: Request): # noqa: PLR0915
|
|||
|
||||
# User is Authe'd in - generate key for the UI to access Proxy
|
||||
verbose_proxy_logger.info(f"SSO callback result: {result}")
|
||||
|
||||
user_email: Optional[str] = getattr(result, "email", None)
|
||||
user_id: Optional[str] = getattr(result, "id", None) if result is not None else None
|
||||
|
||||
|
|
@ -612,6 +649,13 @@ async def auth_callback(request: Request): # noqa: PLR0915
|
|||
budget_duration=internal_user_budget_duration,
|
||||
)
|
||||
|
||||
# (IF SET) Verify user is in restricted SSO group
|
||||
SSOAuthenticationHandler.verify_user_in_restricted_sso_group(
|
||||
general_settings=general_settings,
|
||||
result=result,
|
||||
received_response=received_response,
|
||||
)
|
||||
|
||||
user_info = await get_user_info_from_db(
|
||||
result=result,
|
||||
prisma_client=prisma_client,
|
||||
|
|
@ -1055,6 +1099,44 @@ class SSOAuthenticationHandler:
|
|||
sso_teams = getattr(result, "team_ids", [])
|
||||
await add_missing_team_member(user_info=user_info, sso_teams=sso_teams)
|
||||
|
||||
@staticmethod
|
||||
def verify_user_in_restricted_sso_group(
|
||||
general_settings: Dict,
|
||||
result: Optional[Union[CustomOpenID, OpenID, dict]],
|
||||
received_response: Optional[dict],
|
||||
) -> Literal[True]:
|
||||
"""
|
||||
when ui_access_mode.type == "restricted_sso_group":
|
||||
|
||||
- result.team_ids should contain the restricted_sso_group
|
||||
- if not, raise a ProxyException
|
||||
- if so, return True
|
||||
- if result.team_ids is None, return False
|
||||
- if result.team_ids is an empty list, return False
|
||||
- if result.team_ids is a list, return True if the restricted_sso_group is in the list, otherwise return False
|
||||
"""
|
||||
|
||||
ui_access_mode = cast(
|
||||
Optional[Union[Dict, str]], general_settings.get("ui_access_mode")
|
||||
)
|
||||
|
||||
if ui_access_mode is None:
|
||||
return True
|
||||
if isinstance(ui_access_mode, str):
|
||||
return True
|
||||
team_ids = getattr(result, "team_ids", [])
|
||||
|
||||
if ui_access_mode.get("type") == "restricted_sso_group":
|
||||
restricted_sso_group = ui_access_mode.get("restricted_sso_group")
|
||||
if restricted_sso_group not in team_ids:
|
||||
raise ProxyException(
|
||||
message=f"User is not in the restricted SSO group: {restricted_sso_group}. User groups: {team_ids}. Received SSO response: {received_response}",
|
||||
type=ProxyErrorTypes.auth_error,
|
||||
param="restricted_sso_group",
|
||||
code=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
async def create_litellm_team_from_sso_group(
|
||||
litellm_team_id: str,
|
||||
|
|
@ -1551,7 +1633,29 @@ async def debug_sso_callback(request: Request):
|
|||
|
||||
from fastapi.responses import HTMLResponse
|
||||
|
||||
from litellm.proxy.proxy_server import jwt_handler
|
||||
from litellm.proxy._types import LiteLLM_JWTAuth
|
||||
from litellm.proxy.auth.handle_jwt import JWTHandler
|
||||
from litellm.proxy.proxy_server import (
|
||||
general_settings,
|
||||
jwt_handler,
|
||||
prisma_client,
|
||||
user_api_key_cache,
|
||||
)
|
||||
|
||||
sso_jwt_handler: Optional[JWTHandler] = None
|
||||
ui_access_mode = general_settings.get("ui_access_mode", None)
|
||||
if ui_access_mode is not None and isinstance(ui_access_mode, dict):
|
||||
sso_jwt_handler = JWTHandler()
|
||||
sso_jwt_handler.update_environment(
|
||||
prisma_client=prisma_client,
|
||||
user_api_key_cache=user_api_key_cache,
|
||||
litellm_jwtauth=LiteLLM_JWTAuth(
|
||||
team_ids_jwt_field=general_settings.get("ui_access_mode", {}).get(
|
||||
"sso_group_jwt_field", None
|
||||
),
|
||||
),
|
||||
leeway=0,
|
||||
)
|
||||
|
||||
microsoft_client_id = os.getenv("MICROSOFT_CLIENT_ID", None)
|
||||
google_client_id = os.getenv("GOOGLE_CLIENT_ID", None)
|
||||
|
|
@ -1580,11 +1684,12 @@ async def debug_sso_callback(request: Request):
|
|||
)
|
||||
|
||||
elif generic_client_id is not None:
|
||||
result = await get_generic_sso_response(
|
||||
result, _ = await get_generic_sso_response(
|
||||
request=request,
|
||||
jwt_handler=jwt_handler,
|
||||
generic_client_id=generic_client_id,
|
||||
redirect_url=redirect_url,
|
||||
sso_jwt_handler=sso_jwt_handler,
|
||||
)
|
||||
|
||||
# If result is None, return a basic error message
|
||||
|
|
|
|||
|
|
@ -905,7 +905,7 @@ health_check_results: Dict[str, Union[int, List[Dict[str, Any]]]] = {}
|
|||
queue: List = []
|
||||
litellm_proxy_budget_name = "litellm-proxy-budget"
|
||||
litellm_proxy_admin_name = LITELLM_PROXY_ADMIN_NAME
|
||||
ui_access_mode: Literal["admin", "all"] = "all"
|
||||
ui_access_mode: Union[Literal["admin", "all"], Dict] = "all"
|
||||
proxy_budget_rescheduler_min_time = PROXY_BUDGET_RESCHEDULER_MIN_TIME
|
||||
proxy_budget_rescheduler_max_time = PROXY_BUDGET_RESCHEDULER_MAX_TIME
|
||||
proxy_batch_write_at = PROXY_BATCH_WRITE_AT
|
||||
|
|
@ -1435,11 +1435,13 @@ class ProxyConfig:
|
|||
- Do not write restricted params like 'api_key' to the database
|
||||
- if api_key is passed, save that to the local environment or connected secret manage (maybe expose `litellm.save_secret()`)
|
||||
"""
|
||||
|
||||
if prisma_client is not None and (
|
||||
general_settings.get("store_model_in_db", False) is True
|
||||
or store_model_in_db
|
||||
):
|
||||
# if using - db for config - models are in ModelTable
|
||||
|
||||
new_config.pop("model_list", None)
|
||||
await prisma_client.insert_data(data=new_config, table_name="config")
|
||||
else:
|
||||
|
|
@ -2625,6 +2627,10 @@ class ProxyConfig:
|
|||
pass_through_endpoints=general_settings["pass_through_endpoints"]
|
||||
)
|
||||
|
||||
## UI ACCESS MODE ##
|
||||
if "ui_access_mode" in _general_settings:
|
||||
general_settings["ui_access_mode"] = _general_settings["ui_access_mode"]
|
||||
|
||||
def _update_config_fields(
|
||||
self,
|
||||
current_config: dict,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,10 @@ import litellm
|
|||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.proxy._types import *
|
||||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||
from litellm.types.proxy.management_endpoints.ui_sso import DefaultTeamSSOParams, SSOConfig
|
||||
from litellm.types.proxy.management_endpoints.ui_sso import (
|
||||
DefaultTeamSSOParams,
|
||||
SSOConfig,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
|
@ -18,26 +21,29 @@ class IPAddress(BaseModel):
|
|||
|
||||
class SettingsResponse(BaseModel):
|
||||
"""Base response model for settings with values and schema information"""
|
||||
|
||||
|
||||
values: Dict[str, Any]
|
||||
"""The current configuration values"""
|
||||
|
||||
|
||||
field_schema: Dict[str, Any]
|
||||
"""Schema information including descriptions and property types for UI display"""
|
||||
|
||||
|
||||
class SSOSettingsResponse(SettingsResponse):
|
||||
"""Response model for SSO settings"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class InternalUserSettingsResponse(SettingsResponse):
|
||||
"""Response model for internal user settings"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class DefaultTeamSettingsResponse(SettingsResponse):
|
||||
"""Response model for default team settings"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
|
|
@ -166,7 +172,10 @@ async def _get_settings_with_schema(
|
|||
# Add descriptions to the response
|
||||
result = {
|
||||
"values": settings_dict,
|
||||
"field_schema": {"description": schema.get("description", ""), "properties": {}},
|
||||
"field_schema": {
|
||||
"description": schema.get("description", ""),
|
||||
"properties": {},
|
||||
},
|
||||
}
|
||||
|
||||
# Add property descriptions
|
||||
|
|
@ -322,20 +331,21 @@ async def get_sso_settings():
|
|||
Returns a structured object with values and descriptions for UI display.
|
||||
"""
|
||||
import os
|
||||
|
||||
from litellm.proxy.proxy_server import proxy_config
|
||||
|
||||
|
||||
# Load existing config to get both environment variables and general settings
|
||||
config = await proxy_config.get_config()
|
||||
general_settings = config.get("general_settings", {}) or {}
|
||||
environment_variables = config.get("environment_variables", {}) or {}
|
||||
|
||||
|
||||
# Get user_email from general_settings
|
||||
proxy_admin_email = general_settings.get("proxy_admin_email", None)
|
||||
|
||||
|
||||
# Helper function to get env var value (first from config, then from environment)
|
||||
def get_env_value(env_var_name: str):
|
||||
return environment_variables.get(env_var_name) or os.getenv(env_var_name)
|
||||
|
||||
|
||||
# Get current environment variables for SSO
|
||||
sso_config = SSOConfig(
|
||||
google_client_id=get_env_value("GOOGLE_CLIENT_ID"),
|
||||
|
|
@ -351,27 +361,31 @@ async def get_sso_settings():
|
|||
proxy_base_url=get_env_value("PROXY_BASE_URL"),
|
||||
user_email=proxy_admin_email, # Get from config instead of environment
|
||||
)
|
||||
|
||||
|
||||
# Get the schema for UI display
|
||||
from pydantic import TypeAdapter
|
||||
|
||||
schema = TypeAdapter(SSOConfig).json_schema(by_alias=True)
|
||||
|
||||
|
||||
# Convert to dict for response
|
||||
sso_dict = sso_config.model_dump()
|
||||
|
||||
|
||||
# Add descriptions to the response
|
||||
result = {
|
||||
"values": sso_dict,
|
||||
"field_schema": {"description": schema.get("description", ""), "properties": {}},
|
||||
"field_schema": {
|
||||
"description": schema.get("description", ""),
|
||||
"properties": {},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# Add property descriptions
|
||||
for field_name, field_info in schema["properties"].items():
|
||||
result["field_schema"]["properties"][field_name] = {
|
||||
"description": field_info.get("description", ""),
|
||||
"type": field_info.get("type", "string"),
|
||||
}
|
||||
|
||||
|
||||
return result
|
||||
|
||||
|
||||
|
|
@ -384,51 +398,56 @@ async def update_sso_settings(sso_config: SSOConfig):
|
|||
"""
|
||||
Update SSO configuration by saving to both environment variables and config file.
|
||||
"""
|
||||
from litellm.proxy.proxy_server import proxy_config
|
||||
import os
|
||||
|
||||
|
||||
from litellm.proxy.proxy_server import proxy_config
|
||||
|
||||
# Update environment variables
|
||||
env_var_mapping = {
|
||||
'google_client_id': 'GOOGLE_CLIENT_ID',
|
||||
'google_client_secret': 'GOOGLE_CLIENT_SECRET',
|
||||
'microsoft_client_id': 'MICROSOFT_CLIENT_ID',
|
||||
'microsoft_client_secret': 'MICROSOFT_CLIENT_SECRET',
|
||||
'microsoft_tenant': 'MICROSOFT_TENANT',
|
||||
'generic_client_id': 'GENERIC_CLIENT_ID',
|
||||
'generic_client_secret': 'GENERIC_CLIENT_SECRET',
|
||||
'generic_authorization_endpoint': 'GENERIC_AUTHORIZATION_ENDPOINT',
|
||||
'generic_token_endpoint': 'GENERIC_TOKEN_ENDPOINT',
|
||||
'generic_userinfo_endpoint': 'GENERIC_USERINFO_ENDPOINT',
|
||||
'proxy_base_url': 'PROXY_BASE_URL',
|
||||
"google_client_id": "GOOGLE_CLIENT_ID",
|
||||
"google_client_secret": "GOOGLE_CLIENT_SECRET",
|
||||
"microsoft_client_id": "MICROSOFT_CLIENT_ID",
|
||||
"microsoft_client_secret": "MICROSOFT_CLIENT_SECRET",
|
||||
"microsoft_tenant": "MICROSOFT_TENANT",
|
||||
"generic_client_id": "GENERIC_CLIENT_ID",
|
||||
"generic_client_secret": "GENERIC_CLIENT_SECRET",
|
||||
"generic_authorization_endpoint": "GENERIC_AUTHORIZATION_ENDPOINT",
|
||||
"generic_token_endpoint": "GENERIC_TOKEN_ENDPOINT",
|
||||
"generic_userinfo_endpoint": "GENERIC_USERINFO_ENDPOINT",
|
||||
"proxy_base_url": "PROXY_BASE_URL",
|
||||
}
|
||||
|
||||
|
||||
# Load existing config
|
||||
config = await proxy_config.get_config()
|
||||
|
||||
|
||||
# Update config with new environment variables
|
||||
if "environment_variables" not in config:
|
||||
config["environment_variables"] = {}
|
||||
|
||||
|
||||
# Update general_settings for user_email (admin email)
|
||||
if "general_settings" not in config:
|
||||
config["general_settings"] = {}
|
||||
|
||||
|
||||
# Update environment variables in config and in memory
|
||||
sso_data = sso_config.model_dump(exclude_none=True)
|
||||
for field_name, value in sso_data.items():
|
||||
if field_name == 'user_email' and value is not None:
|
||||
|
||||
if field_name == "user_email" and value is not None:
|
||||
# Store user_email in general_settings instead of environment variables
|
||||
config["general_settings"]["proxy_admin_email"] = value
|
||||
elif field_name == "ui_access_mode" and value is not None:
|
||||
|
||||
config["general_settings"]["ui_access_mode"] = value
|
||||
elif field_name in env_var_mapping and value is not None:
|
||||
env_var_name = env_var_mapping[field_name]
|
||||
# Update in config
|
||||
config["environment_variables"][env_var_name] = value
|
||||
# Update in runtime environment
|
||||
os.environ[env_var_name] = value
|
||||
|
||||
|
||||
# Save the updated config
|
||||
await proxy_config.save_config(new_config=config)
|
||||
|
||||
|
||||
return {
|
||||
"message": "SSO settings updated successfully",
|
||||
"status": "success",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from typing import List, Literal, Optional, TypedDict
|
||||
from typing import List, Literal, Optional, TypedDict, Union
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
|
|
@ -31,6 +31,14 @@ class MicrosoftServicePrincipalTeam(TypedDict, total=False):
|
|||
principalId: Optional[str]
|
||||
|
||||
|
||||
class AccessControl_UI_AccessMode(LiteLLMPydanticObjectBase):
|
||||
"""Model for Controlling UI Access Mode via SSO Groups"""
|
||||
|
||||
type: Literal["restricted_sso_group"]
|
||||
restricted_sso_group: str
|
||||
sso_group_jwt_field: str
|
||||
|
||||
|
||||
class SSOConfig(LiteLLMPydanticObjectBase):
|
||||
"""
|
||||
Configuration for SSO environment variables and settings
|
||||
|
|
@ -45,7 +53,7 @@ class SSOConfig(LiteLLMPydanticObjectBase):
|
|||
default=None,
|
||||
description="Google OAuth Client Secret for SSO authentication",
|
||||
)
|
||||
|
||||
|
||||
# Microsoft SSO
|
||||
microsoft_client_id: Optional[str] = Field(
|
||||
default=None,
|
||||
|
|
@ -59,7 +67,7 @@ class SSOConfig(LiteLLMPydanticObjectBase):
|
|||
default=None,
|
||||
description="Microsoft Azure Tenant ID for SSO authentication",
|
||||
)
|
||||
|
||||
|
||||
# Generic/Okta SSO
|
||||
generic_client_id: Optional[str] = Field(
|
||||
default=None,
|
||||
|
|
@ -81,7 +89,7 @@ class SSOConfig(LiteLLMPydanticObjectBase):
|
|||
default=None,
|
||||
description="User info endpoint URL for generic OAuth provider",
|
||||
)
|
||||
|
||||
|
||||
# Common settings
|
||||
proxy_base_url: Optional[str] = Field(
|
||||
default=None,
|
||||
|
|
@ -92,6 +100,12 @@ class SSOConfig(LiteLLMPydanticObjectBase):
|
|||
description="Email of the proxy admin user",
|
||||
)
|
||||
|
||||
# Access Mode
|
||||
ui_access_mode: Optional[Union[AccessControl_UI_AccessMode, str]] = Field(
|
||||
default=None,
|
||||
description="Access mode for the UI",
|
||||
)
|
||||
|
||||
|
||||
class DefaultTeamSSOParams(LiteLLMPydanticObjectBase):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -696,11 +696,12 @@ async def test_get_generic_sso_response_with_additional_headers():
|
|||
"fastapi_sso.sso.generic.create_provider", return_value=mock_sso_class
|
||||
) as mock_create_provider:
|
||||
# Act
|
||||
result = await get_generic_sso_response(
|
||||
result, received_response = await get_generic_sso_response(
|
||||
request=mock_request,
|
||||
jwt_handler=mock_jwt_handler,
|
||||
generic_client_id=generic_client_id,
|
||||
redirect_url=redirect_url,
|
||||
sso_jwt_handler=None,
|
||||
)
|
||||
|
||||
# Assert
|
||||
|
|
@ -756,11 +757,12 @@ async def test_get_generic_sso_response_with_empty_headers():
|
|||
"fastapi_sso.sso.generic.create_provider", return_value=mock_sso_class
|
||||
) as mock_create_provider:
|
||||
# Act
|
||||
result = await get_generic_sso_response(
|
||||
result, received_response = await get_generic_sso_response(
|
||||
request=mock_request,
|
||||
jwt_handler=mock_jwt_handler,
|
||||
generic_client_id=generic_client_id,
|
||||
redirect_url=redirect_url,
|
||||
sso_jwt_handler=None,
|
||||
)
|
||||
|
||||
# Assert
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ const SSOModals: React.FC<SSOModalsProps> = ({
|
|||
}
|
||||
}
|
||||
|
||||
// Set form values with existing data
|
||||
// Set form values with existing data (excluding UI access control fields)
|
||||
const formValues = {
|
||||
sso_provider: selectedProvider,
|
||||
proxy_base_url: ssoData.values.proxy_base_url,
|
||||
|
|
|
|||
148
ui/litellm-dashboard/src/components/UIAccessControlForm.tsx
Normal file
148
ui/litellm-dashboard/src/components/UIAccessControlForm.tsx
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { Form, Button as Button2, Select, message } from "antd";
|
||||
import { Text, TextInput } from "@tremor/react";
|
||||
import { getSSOSettings, updateSSOSettings } from "./networking";
|
||||
|
||||
interface UIAccessControlFormProps {
|
||||
accessToken: string | null;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
// Separate UI Access Control Form Component
|
||||
const UIAccessControlForm: React.FC<UIAccessControlFormProps> = ({ accessToken, onSuccess }) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Load existing UI access control settings
|
||||
useEffect(() => {
|
||||
const loadUIAccessSettings = async () => {
|
||||
if (accessToken) {
|
||||
try {
|
||||
const ssoData = await getSSOSettings(accessToken);
|
||||
if (ssoData && ssoData.values) {
|
||||
// Handle nested ui_access_mode structure
|
||||
const uiAccessMode = ssoData.values.ui_access_mode;
|
||||
let formValues = {};
|
||||
|
||||
if (uiAccessMode && typeof uiAccessMode === 'object') {
|
||||
formValues = {
|
||||
ui_access_mode_type: uiAccessMode.type,
|
||||
restricted_sso_group: uiAccessMode.restricted_sso_group,
|
||||
sso_group_jwt_field: uiAccessMode.sso_group_jwt_field,
|
||||
};
|
||||
} else if (typeof uiAccessMode === 'string') {
|
||||
// Handle legacy flat structure
|
||||
formValues = {
|
||||
ui_access_mode_type: uiAccessMode,
|
||||
restricted_sso_group: ssoData.values.restricted_sso_group,
|
||||
sso_group_jwt_field: ssoData.values.team_ids_jwt_field || ssoData.values.sso_group_jwt_field,
|
||||
};
|
||||
}
|
||||
|
||||
form.setFieldsValue(formValues);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load UI access settings:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadUIAccessSettings();
|
||||
}, [accessToken, form]);
|
||||
|
||||
const handleUIAccessSubmit = async (formValues: Record<string, any>) => {
|
||||
if (!accessToken) {
|
||||
message.error("No access token available");
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
// Transform form data to match API expected structure
|
||||
const apiPayload = {
|
||||
ui_access_mode: {
|
||||
type: formValues.ui_access_mode_type,
|
||||
restricted_sso_group: formValues.restricted_sso_group,
|
||||
sso_group_jwt_field: formValues.sso_group_jwt_field,
|
||||
}
|
||||
};
|
||||
|
||||
await updateSSOSettings(accessToken, apiPayload);
|
||||
onSuccess();
|
||||
} catch (error) {
|
||||
console.error("Failed to save UI access settings:", error);
|
||||
message.error("Failed to save UI access settings");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '16px' }}>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<Text style={{ fontSize: '14px', color: '#6b7280' }}>
|
||||
Configure who can access the UI interface and how group information is extracted from JWT tokens.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={handleUIAccessSubmit}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form.Item
|
||||
label="UI Access Mode"
|
||||
name="ui_access_mode_type"
|
||||
tooltip="Controls who can access the UI interface"
|
||||
>
|
||||
<Select placeholder="Select access mode">
|
||||
<Select.Option value="all_authenticated_users">All Authenticated Users</Select.Option>
|
||||
<Select.Option value="restricted_sso_group">Restricted SSO Group</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
noStyle
|
||||
shouldUpdate={(prevValues, currentValues) => prevValues.ui_access_mode_type !== currentValues.ui_access_mode_type}
|
||||
>
|
||||
{({ getFieldValue }) => {
|
||||
const uiAccessModeType = getFieldValue('ui_access_mode_type');
|
||||
return uiAccessModeType === 'restricted_sso_group' ? (
|
||||
<Form.Item
|
||||
label="Restricted SSO Group"
|
||||
name="restricted_sso_group"
|
||||
rules={[{ required: true, message: "Please enter the restricted SSO group" }]}
|
||||
>
|
||||
<TextInput placeholder="ui-access-group" />
|
||||
</Form.Item>
|
||||
) : null;
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="SSO Group JWT Field"
|
||||
name="sso_group_jwt_field"
|
||||
tooltip="JWT field name that contains team/group information. Use dot notation to access nested fields."
|
||||
>
|
||||
<TextInput placeholder="groups" />
|
||||
</Form.Item>
|
||||
|
||||
<div style={{ textAlign: "right", marginTop: "16px" }}>
|
||||
<Button2
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={loading}
|
||||
style={{
|
||||
backgroundColor: '#6366f1',
|
||||
borderColor: '#6366f1'
|
||||
}}
|
||||
>
|
||||
Update UI Access Control
|
||||
</Button2>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UIAccessControlForm;
|
||||
|
|
@ -44,6 +44,7 @@ import { InvitationLink } from "./onboarding_link";
|
|||
import SSOModals from "./SSOModals";
|
||||
import { ssoProviderConfigs } from './SSOModals';
|
||||
import SCIMConfig from "./SCIM";
|
||||
import UIAccessControlForm from "./UIAccessControlForm";
|
||||
|
||||
interface AdminPanelProps {
|
||||
searchParams: any;
|
||||
|
|
@ -97,6 +98,7 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
|
|||
const [isAllowedIPModalVisible, setIsAllowedIPModalVisible] = useState(false);
|
||||
const [isAddIPModalVisible, setIsAddIPModalVisible] = useState(false);
|
||||
const [isDeleteIPModalVisible, setIsDeleteIPModalVisible] = useState(false);
|
||||
const [isUIAccessControlModalVisible, setIsUIAccessControlModalVisible] = useState(false);
|
||||
const [allowedIPs, setAllowedIPs] = useState<string[]>([]);
|
||||
const [ipToDelete, setIPToDelete] = useState<string | null>(null);
|
||||
const [ssoConfigured, setSsoConfigured] = useState<boolean>(false);
|
||||
|
|
@ -532,6 +534,14 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
const handleUIAccessControlOk = () => {
|
||||
setIsUIAccessControlModalVisible(false);
|
||||
};
|
||||
|
||||
const handleUIAccessControlCancel = () => {
|
||||
setIsUIAccessControlModalVisible(false);
|
||||
};
|
||||
|
||||
console.log(`admins: ${admins?.length}`);
|
||||
return (
|
||||
<div className="w-full m-2 mt-2 p-8">
|
||||
|
|
@ -563,6 +573,14 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
|
|||
Allowed IPs
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
style={{ width: '150px' }}
|
||||
onClick={() => premiumUser === true ? setIsUIAccessControlModalVisible(true) : message.error("Only premium users can configure UI access control")}
|
||||
>
|
||||
UI Access Control
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
|
|
@ -654,6 +672,24 @@ const AdminPanel: React.FC<AdminPanelProps> = ({
|
|||
>
|
||||
<p>Are you sure you want to delete the IP address: {ipToDelete}?</p>
|
||||
</Modal>
|
||||
|
||||
{/* UI Access Control Modal */}
|
||||
<Modal
|
||||
title="UI Access Control Settings"
|
||||
visible={isUIAccessControlModalVisible}
|
||||
width={600}
|
||||
footer={null}
|
||||
onOk={handleUIAccessControlOk}
|
||||
onCancel={handleUIAccessControlCancel}
|
||||
>
|
||||
<UIAccessControlForm
|
||||
accessToken={accessToken}
|
||||
onSuccess={() => {
|
||||
handleUIAccessControlOk();
|
||||
message.success("UI Access Control settings updated successfully");
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
<Callout title="Login without SSO" color="teal">
|
||||
If you need to login without sso, you can access{" "}
|
||||
|
|
|
|||
|
|
@ -260,6 +260,10 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
updateData.team_member_budget = Number(values.team_member_budget);
|
||||
}
|
||||
|
||||
if (values.team_member_key_duration !== undefined) {
|
||||
updateData.team_member_key_duration = values.team_member_key_duration;
|
||||
}
|
||||
|
||||
// Handle object_permission updates
|
||||
if (values.vector_stores !== undefined || values.mcp_servers !== undefined) {
|
||||
updateData.object_permission = {
|
||||
|
|
@ -453,6 +457,15 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
<NumericalInput step={0.01} precision={2} style={{ width: "100%" }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Team Member Key Duration" name="team_member_key_duration" tooltip="Set a limit to the duration of a team member's key.">
|
||||
<Select placeholder="n/a">
|
||||
<Select.Option value="1d">1 day</Select.Option>
|
||||
<Select.Option value="1w">1 week</Select.Option>
|
||||
<Select.Option value="1mo">1 month</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
|
||||
<Form.Item label="Reset Budget" name="budget_duration">
|
||||
<Select placeholder="n/a">
|
||||
<Select.Option value="24h">daily</Select.Option>
|
||||
|
|
@ -561,9 +574,19 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
<div>RPM: {info.rpm_limit || 'Unlimited'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="font-medium">Budget</Text>
|
||||
<div>Max: {info.max_budget !== null ? `$${info.max_budget}` : 'No Limit'}</div>
|
||||
<div>Reset: {info.budget_duration || 'Never'}</div>
|
||||
<Text className="font-medium">Team Budget</Text>
|
||||
<div>Max Budget: {info.max_budget !== null ? `$${info.max_budget}` : 'No Limit'}</div>
|
||||
<div>Budget Reset: {info.budget_duration || 'Never'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="font-medium">
|
||||
Team Member Settings{' '}
|
||||
<Tooltip title="These are limits on individual team members">
|
||||
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
|
||||
</Tooltip>
|
||||
</Text>
|
||||
<div>Max Budget: {info.team_member_budget_table?.max_budget || 'No Limit'}</div>
|
||||
<div>Key Duration: {info.metadata?.team_member_key_duration || 'No Limit'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text className="font-medium">Organization ID</Text>
|
||||
|
|
|
|||
|
|
@ -1060,6 +1060,17 @@ const Teams: React.FC<TeamProps> = ({
|
|||
>
|
||||
<NumericalInput step={0.01} precision={2} width={200} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Team Member Key Duration"
|
||||
name="team_member_key_duration"
|
||||
tooltip="Set a limit to the duration of a team member's key."
|
||||
>
|
||||
<Select2 defaultValue={null} placeholder="n/a">
|
||||
<Select2.Option value="1d">1 day</Select2.Option>
|
||||
<Select2.Option value="1w">1 week</Select2.Option>
|
||||
<Select2.Option value="1mo">1 month</Select2.Option>
|
||||
</Select2>
|
||||
</Form.Item>
|
||||
<Form.Item label="Metadata" name="metadata" help="Additional team metadata. Enter metadata as JSON object.">
|
||||
<Input.TextArea rows={4} />
|
||||
</Form.Item>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue