test(chat): cover signed-in team request authorization (#35187)
Some checks failed
LiteLLM Rust DB / db-queries (push) Has been cancelled
LiteLLM Rust / rust-lint (push) Has been cancelled
LiteLLM Rust / rust-test (push) Has been cancelled
LiteLLM Rust / rust-wheel (push) Has been cancelled

This commit is contained in:
ishaan-j-berri 2026-10-27 14:42:36 -07:00
parent 46fc5fd1ac
commit 6210f4bbaa
2 changed files with 272 additions and 50 deletions

View file

@ -1,4 +1,5 @@
from typing import cast
from unittest.mock import AsyncMock
import pytest
from fastapi.encoders import jsonable_encoder
@ -28,9 +29,9 @@ from litellm.proxy.utils import (
get_available_models_for_user,
)
from litellm.types.proxy.management_endpoints.model_management_endpoints import (
ModelGroupTeam,
ModelGroupInfoProxy,
ModelGroupInfoResponse,
ModelGroupTeam,
)
@ -142,6 +143,62 @@ async def test_direct_user_access_is_the_default_without_a_team_selection(accoun
assert await access.for_request(("personal",), None) == account[0]
async def test_session_model_access_fails_closed_without_a_database(account, router):
auth, _, _, _, _, cache, logging = account
with pytest.raises(ProxyException) as error:
await load_ui_session_model_sources(auth, router, None, cache, logging)
assert (error.value.code, error.value.type, error.value.param) == ("503", "no_db_connection", None)
async def test_deleted_user_cannot_reuse_model_grants_from_the_session(account, router, monkeypatch):
auth, _, _, _, _, cache, logging = account
prisma = PrismaClient(database_url="postgresql://localhost/session-tests", proxy_logging_obj=logging)
missing_user = AsyncMock(return_value=None)
monkeypatch.setattr("prisma.actions.LiteLLM_UserTableActions.find_unique", missing_user)
deleted_user_auth = auth.model_copy(update={"user_id": "deleted-session-user"})
with pytest.raises(ProxyException) as error:
await load_ui_session_model_sources(deleted_user_auth, router, prisma, cache, logging)
assert (error.value.code, error.value.type, error.value.param) == ("403", "auth_error", None)
missing_user.assert_awaited_once_with(
where={"user_id": "deleted-session-user"}, include={"organization_memberships": True}
)
async def test_deleted_team_is_not_discoverable_or_selectable_with_stale_membership(account, router, monkeypatch):
auth, _, team, _, _, cache, logging = account
prisma = PrismaClient(database_url="postgresql://localhost/session-tests", proxy_logging_obj=logging)
missing_team = AsyncMock(return_value=None)
monkeypatch.setattr("prisma.actions.LiteLLM_TeamTableActions.find_unique", missing_team)
await cache.async_delete_cache(f"team_id:{team.team_id}")
access = await load_ui_session_model_sources(auth, router, prisma, cache, logging)
assert access is not None
assert await access.available_teams("personal") == ()
assert await access.available_teams("research") is None
with pytest.raises(ProxyException) as error:
await access.for_request(("research",), team.team_id)
assert (error.value.code, error.value.type, error.value.param) == (
"403",
"team_model_access_denied",
"x-litellm-team-id",
)
missing_team.assert_awaited_once()
async def test_changed_live_user_role_stops_internal_session_grant_resolution(account, router):
auth, user, _, _, prisma, cache, logging = account
await cache.async_set_cache(
key=user.user_id,
value=user.model_copy(update={"user_role": LitellmUserRoles.PROXY_ADMIN}),
model_type=LiteLLM_UserTable,
)
assert await load_ui_session_model_sources(auth, router, prisma, cache, logging) is None
assert auth.user_role == LitellmUserRoles.INTERNAL_USER_VIEW_ONLY
@pytest.mark.parametrize("live_models", [["no-default-models"], ["shared"]])
async def test_removed_user_and_team_grants_deny_even_when_the_session_snapshot_allows(account, router, live_models):
auth, user, _, _, prisma, cache, logging = account

View file

@ -548,8 +548,6 @@ def test_get_api_key_from_custom_header_different_casing():
)
@pytest.mark.parametrize(
"user_role, auth_user_id, requested_user_id, expected_result",
[
@ -963,71 +961,60 @@ async def test_accepted_websocket_reauthorizes_the_frame_model_without_closing()
@pytest.mark.parametrize("enforce_rbac", [True, False])
@pytest.mark.asyncio
async def test_jwt_user_api_key_auth_builder_enforce_rbac(enforce_rbac, monkeypatch):
from litellm.proxy.auth.handle_jwt import JWTHandler, JWTAuthManager
from unittest.mock import patch, Mock
async def test_jwt_user_api_key_auth_builder_enforce_rbac(enforce_rbac: bool, monkeypatch: pytest.MonkeyPatch) -> None:
import jwt as pyjwt
from cryptography.hazmat.primitives.asymmetric import rsa
from litellm.proxy._types import LiteLLM_JWTAuth
from litellm.caching import DualCache
from litellm.proxy.auth.handle_jwt import JWTAuthManager, JWTHandler
from litellm.proxy.common_utils.user_api_key_cache import UserApiKeyCache
from litellm.proxy.utils import ProxyLogging
monkeypatch.setenv("JWT_PUBLIC_KEY_URL", "my-fake-url")
monkeypatch.setenv("JWT_PUBLIC_KEY_URL", "https://idp.example.com/keys")
monkeypatch.setenv("JWT_AUDIENCE", "api://LiteLLM_Proxy-dev")
monkeypatch.delenv("JWT_ISSUER", raising=False)
local_cache = DualCache()
keys = [
{
"kty": "RSA",
"use": "sig",
"kid": "z1rsYHHJ9-8mggt4HsZu8BKkBPw",
"x5t": "z1rsYHHJ9-8mggt4HsZu8BKkBPw",
"n": "pOe4GbleFDT1u5ioOQjNMmhvkDVoVD9cBKvX7AlErtWA_D6wc1w1iwkd6arYVCPObZbAB4vLSXrlpBSOuP6VYnXw_cTgniv_c82ra-mfqCpM-SbqzZ3sVqlcE_bwxvci_4PrxAW4R85ok12NXyZ2371H3yGevabi35AlVm-bQ24azo1hLK_0DzB6TxsAIOTOcKfIugOfqP-B2R4vR4u6pYftS8MWcxegr9iJ5JNtubI1X2JHpxJhkRoMVwKFna2GXmtzdxLi3yS_GffVCKfTbFMhalbJS1lSmLqhmLZZL-lrQZ6fansTl1vcGcoxnzPTwBkZMks0iVV4yfym_gKBXQ",
"e": "AQAB",
"x5c": [
"MIIC/TCCAeWgAwIBAgIIQk8Qok6pfXkwDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDAeFw0yNDExMjcwOTA0MzlaFw0yOTExMjcwOTA0MzlaMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCk57gZuV4UNPW7mKg5CM0yaG+QNWhUP1wEq9fsCUSu1YD8PrBzXDWLCR3pqthUI85tlsAHi8tJeuWkFI64/pVidfD9xOCeK/9zzatr6Z+oKkz5JurNnexWqVwT9vDG9yL/g+vEBbhHzmiTXY1fJnbfvUffIZ69puLfkCVWb5tDbhrOjWEsr/QPMHpPGwAg5M5wp8i6A5+o/4HZHi9Hi7qlh+1LwxZzF6Cv2Inkk225sjVfYkenEmGRGgxXAoWdrYZea3N3EuLfJL8Z99UIp9NsUyFqVslLWVKYuqGYtlkv6WtBnp9qexOXW9wZyjGfM9PAGRkySzSJVXjJ/Kb+AoFdAgMBAAGjITAfMB0GA1UdDgQWBBSTO5FmUwwGS+1CNqg2uNgjxUjFijANBgkqhkiG9w0BAQsFAAOCAQEAok04z0ICMEHGqDTzx6eD7vvJP8itJTCSz8JcZcGVJofJpViGF3bNnyeSPa7vNDYP1Ps9XBvw3/n2s+yynZ8EwFxMyxCZRCSbLv0N+cAbH3rmZqGcgMJszZVwcFUtXQPTe1ZRyHtEyOB+PVFH7K7obysRVO/cC6EGqIF3pYWzez/dtMaXRAkdTNlz0ko62WoA4eMPwUFCITjW/Jxfxl0BNUbo82PXXKhaeVJb+EgFG5b/pWWPswWmBoQhmD5G1UODvEACHRl/cHsPPqe4YE+6D1/wMno/xqqyGltnk8v0d4TpNcQMn9oM19V+OGgrzWOvvXhvnhqUIVGMsRlyBGNHAw=="
],
"cloud_instance_name": "microsoftonline.com",
"issuer": "https://login.microsoftonline.com/bdfd79b3-8401-47fb-a764-6e595c455b05/v2.0",
}
]
local_cache.set_cache(
key="litellm_jwt_auth_keys_my-fake-url",
value=keys,
)
litellm_jwtauth = LiteLLM_JWTAuth(
**{
"admin_jwt_scope": "litellm_proxy_endpoints_access",
"admin_allowed_routes": ["openai_routes", "info_routes"],
"public_key_ttl": 600,
"enforce_rbac": enforce_rbac,
}
)
jwt_handler = JWTHandler()
private_key: Final = rsa.generate_private_key(public_exponent=65537, key_size=2048)
public_jwk: Final = {
**json.loads(pyjwt.algorithms.RSAAlgorithm.to_jwk(private_key.public_key())),
"kid": "synthetic-rbac-test",
}
claims: Final = {"sub": "synthetic-rbac-user", "aud": "api://LiteLLM_Proxy-dev"}
token: Final = pyjwt.encode(claims, private_key, algorithm="RS256", headers={"kid": "synthetic-rbac-test"})
local_cache: Final = UserApiKeyCache()
local_cache.set_cache(key="litellm_jwt_auth_keys_https://idp.example.com/keys", value=[public_jwk])
jwt_handler: Final = JWTHandler()
jwt_handler.update_environment(
prisma_client=None,
user_api_key_cache=local_cache,
litellm_jwtauth=litellm_jwtauth,
leeway=10000000000000,
litellm_jwtauth=LiteLLM_JWTAuth(
admin_jwt_scope="litellm_proxy_endpoints_access",
admin_allowed_routes=["openai_routes", "info_routes"],
public_key_ttl=600,
enforce_rbac=enforce_rbac,
),
)
args = {
"api_key": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6InoxcnNZSEhKOS04bWdndDRIc1p1OEJLa0JQdyIsImtpZCI6InoxcnNZSEhKOS04bWdndDRIc1p1OEJLa0JQdyJ9.eyJhdWQiOiJhcGk6Ly9MaXRlTExNX1Byb3h5LWRldiIsImlzcyI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0L2JkZmQ3OWIzLTg0MDEtNDdmYi1hNzY0LTZlNTk1YzQ1NWIwNS8iLCJpYXQiOjE3MzcyNDE3ODEsIm5iZiI6MTczNzI0MTc4MSwiZXhwIjoxNzM3MjQ1NjgxLCJhaW8iOiJrMlJnWUpBNE5hZGg4MGJQdXlyRmxlV1o3dHZiQUE9PSIsImFwcGlkIjoiOGNjZjNkMDItMmNkNi00N2I5LTgxODUtMGVkYjI0YWJjZjY5IiwiYXBwaWRhY3IiOiIxIiwiaWRwIjoiaHR0cHM6Ly9zdHMud2luZG93cy5uZXQvYmRmZDc5YjMtODQwMS00N2ZiLWE3NjQtNmU1OTVjNDU1YjA1LyIsIm9pZCI6IjQ0YTg3YTYzLWFiNTUtNDc4NS1iMmFmLTMzNjllZWM4ZTEzOSIsInJoIjoiMS5BYjBBczNuOXZRR0UtMGVuWkc1WlhFVmJCY0VDbkl6NHJxaE9wZ2E0UGZSZjBsbTlBQUM5QUEuIiwic3ViIjoiNDRhODdhNjMtYWI1NS00Nzg1LWIyYWYtMzM2OWVlYzhlMTM5IiwidGlkIjoiYmRmZDc5YjMtODQwMS00N2ZiLWE3NjQtNmU1OTVjNDU1YjA1IiwidXRpIjoiY3ltNVhlcmhIMHVMSlNZU1JyQmhBQSIsInZlciI6IjEuMCJ9.UooJjM9pS-wgYsExqgHdrYyQhp7NbwAsr7au9dWJaLpsufXeyHJSg-Xd5VJ4RsDVJiDes3jkC7WeoAiaCfzEHpAum-p_aqqLYXf1QIYbi1hLC0m7y_klFcqMp11WbDa9TSTvg-o8q3x2Y5su8X23ymlFih4OP17b7JA6a4_2MybU5QkCEW1tQK6VspuuXzeDHvbfGeGYcIptHFyfttHMHHXRtX1o9bX7gOR_dwFITAXD18T4ZdAN_0y6f1OtVF9TMWQhMXhKU8ahn8TSg_CXmPl9T_1gV3ZWLvVtcdVrWs82fDz3-2lEw28z4bQEr1Z5xoAz7srhx1WEBu_ioAcQiA",
args: Final = {
"api_key": token,
"jwt_handler": jwt_handler,
"route": "/v1/chat/completions",
"prisma_client": None,
"user_api_key_cache": Mock(),
"user_api_key_cache": local_cache,
"parent_otel_span": None,
"proxy_logging_obj": Mock(),
"proxy_logging_obj": ProxyLogging(local_cache),
"request_data": {},
"general_settings": {},
}
if enforce_rbac:
with pytest.raises(HTTPException):
with pytest.raises(HTTPException) as denied:
await JWTAuthManager.auth_builder(**args)
assert denied.value.status_code == 403
assert "enforce_rbac is set to True" in denied.value.detail
else:
await JWTAuthManager.auth_builder(**args)
authorized: Final = await JWTAuthManager.auth_builder(**args)
assert authorized["jwt_claims"] == claims
assert authorized["is_proxy_admin"] is False
def test_user_api_key_auth_end_user_str():
@ -1138,7 +1125,7 @@ async def test_jwt_non_admin_team_route_access(monkeypatch):
"org_object": None,
"end_user_id": None,
"end_user_object": None,
"token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJmR09YQTNhbHFObjByRzJ6OHJQT1FLZVVMSWxCNDFnVWl4VDJ5WE1QVG1ZIn0.eyJleHAiOjE3NDI2MDAzODIsImlhdCI6MTc0MjYwMDA4MiwianRpIjoiODRhNjZmZjAtMTE5OC00YmRkLTk1NzAtNWZhMjNhZjYxMmQyIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy9saXRlbGxtLXJlYWxtIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6ImZmMGZjOGNiLWUyMjktNDkyYy05NzYwLWNlYzVhMDYxNmI2MyIsInR5cCI6IkJlYXJlciIsImF6cCI6ImxpdGVsbG0tdGVzdC1jbGllbnQtaWQiLCJzaWQiOiI4MTYwNjIxOC0yNmZmLTQwMjAtOWQxNy05Zjc0YmFlNTBkODUiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbImh0dHA6Ly9sb2NhbGhvc3Q6NDAwMC8qIl0sInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsImRlZmF1bHQtcm9sZXMtbGl0ZWxsbS1yZWFsbSIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJwcm9maWxlIGdyb3Vwcy1zY29wZSBlbWFpbCBsaXRlbGxtLmFwaS5jb25zdW1lciIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJuYW1lIjoiS3Jpc2ggRGhvbGFraWEiLCJncm91cHMiOlsiL28zX21pbmlfYWNjZXNzIl0sInByZWZlcnJlZF91c2VybmFtZSI6ImtycmlzaGRoMiIsImdpdmVuX25hbWUiOiJLcmlzaCIsImZhbWlseV9uYW1lIjoiRGhvbGFraWEiLCJlbWFpbCI6ImtycmlzaGRob2xha2lhMkBnbWFpbC5jb20ifQ.Fu2ErZhnfez-bhn_XmjkywcFdZHcFUSvzIzfdNiEowdA0soLmCyqf9731amP6m68shd9qk11e0mQhxFIAIxZPojViC1Csc9TBXLRRQ8ESMd6gPIj-DBkKVkQSZLJ1uibsh4Oo2RViGtqWVcEt32T8U_xhGdtdzNkJ8qy_e0fdNDsUnhmSaTQvmZJYarW0roIrkC-zYZrX3fftzbQfavSu9eqdfPf6wUttIrkaWThWUuORy-xaeZfSmvsGbEg027hh6QwlChiZTSF8R6bRxoqfPN3ZaGFFgbBXNRYZA_eYi2IevhIwJHi_r4o1UvtKAJyfPefm-M6hCfkN_6da4zsog",
"token": "fake.jwt.token",
}
# Create request
@ -2282,3 +2269,181 @@ async def test_non_object_metadata_is_served_like_unset_mappings_whatever_the_ba
)
assert mapped == unset
assert mapped[1] == "usecase-deployment"
@pytest.fixture
async def cached_chat_session(monkeypatch: pytest.MonkeyPatch) -> UserAPIKeyAuth:
from typing import cast
from litellm.constants import UI_SESSION_TOKEN_TEAM_ID
from litellm.proxy._types import LiteLLM_TeamMembership, LiteLLM_TeamTableCachedObj
from litellm.proxy.common_utils.user_api_key_cache import (
UserApiKeyCache,
team_membership_auth_cache_key,
team_membership_reservation_cache_key,
)
from litellm.proxy.utils import PrismaClient, ProxyLogging
cache: Final = UserApiKeyCache()
router: Final = litellm.Router(
model_list=[
{
"model_name": name,
"litellm_params": {"model": "openai/gpt-4.1-mini", "api_key": "test-key"},
}
for name in ("personal", "research", "clinical", "shared", "forbidden")
]
)
user: Final = LiteLLM_UserTable(
user_id="researcher",
user_role=LitellmUserRoles.INTERNAL_USER_VIEW_ONLY,
models=["personal"],
teams=["research-team", "clinical-team"],
max_budget=10,
)
await cache.async_set_cache(key=user.user_id, value=user, model_type=LiteLLM_UserTable)
for team in (
LiteLLM_TeamTableCachedObj(
team_id="research-team", team_alias="Research", models=["research", "shared"], max_budget=20
),
LiteLLM_TeamTableCachedObj(
team_id="clinical-team", team_alias="Clinical", models=["clinical", "shared"], max_budget=30
),
):
member: Final = LiteLLM_TeamMembership(user_id=user.user_id, team_id=team.team_id, spend=1)
await cache.async_set_cache(key=f"team_id:{team.team_id}", value=team, model_type=LiteLLM_TeamTableCachedObj)
await cache.async_set_cache(key=team_membership_reservation_cache_key(user.user_id, team.team_id), value=member)
await cache.async_set_cache(
key=team_membership_auth_cache_key(team_id=team.team_id, user_id=user.user_id),
value=member,
model_type=LiteLLM_TeamMembership,
)
monkeypatch.setattr(litellm.proxy.proxy_server, "prisma_client", cast(PrismaClient, object()))
monkeypatch.setattr(litellm.proxy.proxy_server, "user_api_key_cache", cache)
monkeypatch.setattr(litellm.proxy.proxy_server, "proxy_logging_obj", ProxyLogging(cache))
monkeypatch.setattr(litellm.proxy.proxy_server, "llm_router", router)
monkeypatch.setattr(litellm.proxy.proxy_server, "llm_model_list", router.model_list)
monkeypatch.setattr(litellm.proxy.proxy_server, "general_settings", {})
monkeypatch.setattr(litellm, "max_budget", 0)
return UserAPIKeyAuth(
token="session-hash",
user_id=user.user_id,
team_id=UI_SESSION_TOKEN_TEAM_ID,
user_role=LitellmUserRoles.INTERNAL_USER_VIEW_ONLY,
models=["personal"],
max_budget=3,
)
async def _validated_chat_request(
auth: UserAPIKeyAuth, request_data: dict[str, object], selected_team_id: str | None = None
) -> UserAPIKeyAuth:
from datetime import datetime, timezone
from fastapi import Request as FastAPIRequest
from litellm.proxy.auth.user_api_key_auth import validate_resolved_virtual_key
request: Final = FastAPIRequest(
{
"type": "http",
"method": "POST",
"path": "/v1/chat/completions",
"headers": ([(b"x-litellm-team-id", selected_team_id.encode())] if selected_team_id is not None else []),
"query_string": b"",
}
)
return await validate_resolved_virtual_key(
request=request,
request_data=request_data,
valid_token=auth,
api_key="session-hash",
route="/v1/chat/completions",
start_time=datetime(2026, 1, 1, tzinfo=timezone.utc),
parent_otel_span=None,
end_user_id=None,
end_user_params={},
_end_user_object=None,
)
@pytest.mark.asyncio
@pytest.mark.parametrize("selected_team_id", ["clinical-team", None], ids=["selected", "unique-fallback-grant"])
async def test_chat_session_request_binds_selected_team_and_preserves_user_and_key_budgets(
cached_chat_session: UserAPIKeyAuth,
selected_team_id: str | None,
) -> None:
from litellm.constants import UI_SESSION_TOKEN_TEAM_ID
authorized: Final = await _validated_chat_request(
cached_chat_session, {"model": "shared", "fallbacks": ["clinical"]}, selected_team_id
)
assert (
authorized.user_id,
authorized.team_id,
authorized.team_alias,
authorized.max_budget,
authorized.user_max_budget,
authorized.team_max_budget,
authorized.team_member_spend,
authorized.via_virtual_key,
) == ("researcher", "clinical-team", "Clinical", 3, 10, 30, 1, True)
assert (cached_chat_session.team_id, cached_chat_session.models) == (UI_SESSION_TOKEN_TEAM_ID, ["personal"])
@pytest.mark.asyncio
@pytest.mark.parametrize("fallback", ["research", {"model": "research"}])
async def test_chat_session_request_cannot_use_another_teams_fallback(
cached_chat_session: UserAPIKeyAuth, fallback: str | dict[str, str]
) -> None:
from litellm.proxy._types import ProxyException
with pytest.raises(ProxyException) as denied:
await _validated_chat_request(
cached_chat_session, {"model": "shared", "fallbacks": [fallback]}, "clinical-team"
)
assert (denied.value.code, denied.value.type, denied.value.param) == (
"403",
"team_model_access_denied",
"x-litellm-team-id",
)
@pytest.mark.asyncio
async def test_chat_session_request_requires_team_selection_for_shared_model(
cached_chat_session: UserAPIKeyAuth,
) -> None:
from litellm.proxy._types import ProxyException
with pytest.raises(ProxyException) as denied:
await _validated_chat_request(cached_chat_session, {"model": "shared"})
assert (denied.value.code, denied.value.type) == ("409", "team_selection_required")
@pytest.mark.asyncio
async def test_chat_session_request_still_enforces_original_key_budget_after_team_selection(
cached_chat_session: UserAPIKeyAuth,
) -> None:
exhausted: Final = cached_chat_session.model_copy(update={"spend": 4})
with pytest.raises(litellm.BudgetExceededError) as denied:
await _validated_chat_request(exhausted, {"model": "shared"}, "clinical-team")
assert (denied.value.current_cost, denied.value.max_budget) == (4, 3)
@pytest.mark.asyncio
async def test_chat_team_header_cannot_grant_an_ordinary_key_access_to_a_team_fallback(
cached_chat_session: UserAPIKeyAuth,
) -> None:
from litellm.proxy._types import ProxyException
ordinary_key: Final = cached_chat_session.model_copy(update={"team_id": None})
with pytest.raises(ProxyException) as denied:
await _validated_chat_request(ordinary_key, {"model": "personal", "fallbacks": ["clinical"]}, "clinical-team")
assert (denied.value.code, denied.value.type, denied.value.param) == ("403", "key_model_access_denied", "model")