mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-23 00:41:40 +00:00
* test: drop the cwd-relative sys.path.insert calls from the test suite
TQ003 stands at 1,077 across 1,058 files, and 1,015 of them are the same shape:
sys.path.insert(0, os.path.abspath("../..")) and its deeper siblings. The
argument resolves against the working directory rather than the file, so from
the repo root, where every job runs pytest, it inserts the directory two levels
above the checkout. It has never pointed at litellm. The package is installed
into the environment anyway, which is what actually makes the import work, and
what the rule's message has said all along.
Removing them leaves 1,634 imports of sys and os with no remaining reference,
and those go too, except where another test module imports the name back out of
the file. The rest of TQ003 is 62 call sites that resolve against __file__ or a
variable, which are a different question and are left alone.
Collection is identical either way: 45,871 tests and the same 51 pre-existing
collection errors before and after, and ruff reports no new undefined name.
* test: drop the duplicate imports the sys.path sweep exposed to F811
* test(pre-call-utils): restore the os import the new bedrock tests need
4371 lines
151 KiB
Python
4371 lines
151 KiB
Python
# Test the following scenarios:
|
|
# 1. Generate a Key, and use it to make a call
|
|
# 2. Make a call with invalid key, expect it to fail
|
|
# 3. Make a call to a key with invalid model - expect to fail
|
|
# 4. Make a call to a key with valid model - expect to pass
|
|
# 5. Make a call with user over budget, expect to fail
|
|
# 6. Make a streaming chat/completions call with user over budget, expect to fail
|
|
# 7. Make a call with an key that never expires, expect to pass
|
|
# 8. Make a call with an expired key, expect to fail
|
|
# 9. Delete a Key
|
|
# 10. Generate a key, call key/info. Assert info returned is the same as generated key info
|
|
# 11. Generate a Key, cal key/info, call key/update, call key/info
|
|
# 12. Make a call with key over budget, expect to fail
|
|
# 14. Make a streaming chat/completions call with key over budget, expect to fail
|
|
# 15. Generate key, when `allow_user_auth`=False - check if `/key/info` returns key_name=null
|
|
# 16. Generate key, when `allow_user_auth`=True - check if `/key/info` returns key_name=sk...<last-4-digits>
|
|
|
|
|
|
# function to call to generate key - async def new_user(data: NewUserRequest):
|
|
# function to validate a request - async def user_auth(request: Request):
|
|
|
|
import os
|
|
import re
|
|
import traceback
|
|
from litellm._uuid import uuid
|
|
from datetime import datetime, timezone
|
|
from unittest import mock
|
|
|
|
from dotenv import load_dotenv
|
|
from fastapi import Request
|
|
from fastapi.routing import APIRoute
|
|
import httpx
|
|
|
|
load_dotenv()
|
|
import io
|
|
import time
|
|
|
|
# this file is to test litellm/proxy
|
|
|
|
import asyncio
|
|
import logging
|
|
|
|
import pytest
|
|
|
|
import litellm
|
|
from litellm._logging import verbose_proxy_logger
|
|
from litellm.proxy.management_endpoints.internal_user_endpoints import (
|
|
new_user,
|
|
user_info,
|
|
user_update,
|
|
)
|
|
from litellm.proxy.auth.auth_checks import get_key_object
|
|
from litellm.proxy.management_endpoints.key_management_endpoints import (
|
|
delete_key_fn,
|
|
generate_key_fn,
|
|
generate_key_helper_fn,
|
|
info_key_fn,
|
|
list_keys,
|
|
regenerate_key_fn,
|
|
update_key_fn,
|
|
key_aliases,
|
|
)
|
|
from litellm.proxy.management_endpoints.team_endpoints import (
|
|
new_team,
|
|
team_info,
|
|
update_team,
|
|
)
|
|
from litellm.proxy.proxy_server import (
|
|
LitellmUserRoles,
|
|
audio_transcriptions,
|
|
chat_completion,
|
|
completion,
|
|
embeddings,
|
|
model_list,
|
|
moderations,
|
|
user_api_key_auth,
|
|
)
|
|
from litellm.proxy.image_endpoints import image_generation
|
|
from litellm.proxy.management_endpoints.customer_endpoints import (
|
|
new_end_user,
|
|
)
|
|
from litellm.proxy.spend_tracking.spend_management_endpoints import (
|
|
global_spend,
|
|
spend_key_fn,
|
|
spend_user_fn,
|
|
view_spend_logs,
|
|
)
|
|
from litellm.proxy.utils import PrismaClient, ProxyLogging, hash_token, update_spend
|
|
|
|
verbose_proxy_logger.setLevel(level=logging.DEBUG)
|
|
|
|
from starlette.datastructures import URL
|
|
|
|
from litellm.caching.caching import DualCache
|
|
from litellm.types.proxy.management_endpoints.ui_sso import (
|
|
LiteLLM_UpperboundKeyGenerateParams,
|
|
)
|
|
from litellm.proxy._types import (
|
|
DynamoDBArgs,
|
|
GenerateKeyRequest,
|
|
KeyRequest,
|
|
NewCustomerRequest,
|
|
NewTeamRequest,
|
|
NewUserRequest,
|
|
ProxyErrorTypes,
|
|
ProxyException,
|
|
UpdateKeyRequest,
|
|
UpdateTeamRequest,
|
|
UpdateUserRequest,
|
|
UserAPIKeyAuth,
|
|
)
|
|
|
|
proxy_logging_obj = ProxyLogging(user_api_key_cache=DualCache())
|
|
|
|
|
|
request_data = {
|
|
"model": "azure-gpt-3.5",
|
|
"messages": [
|
|
{"role": "user", "content": "this is my new test. respond in 50 lines"}
|
|
],
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def prisma_client():
|
|
from litellm.proxy.proxy_cli import append_query_params
|
|
|
|
### add connection pool + pool timeout args
|
|
params = {"connection_limit": 100, "pool_timeout": 60}
|
|
database_url = os.getenv("DATABASE_URL")
|
|
modified_url = append_query_params(database_url, params)
|
|
os.environ["DATABASE_URL"] = modified_url
|
|
|
|
# Assuming PrismaClient is a class that needs to be instantiated
|
|
prisma_client = PrismaClient(
|
|
database_url=os.environ["DATABASE_URL"], proxy_logging_obj=proxy_logging_obj
|
|
)
|
|
|
|
# Reset litellm.proxy.proxy_server.prisma_client to None
|
|
litellm.proxy.proxy_server.litellm_proxy_budget_name = (
|
|
f"litellm-proxy-budget-{time.time()}"
|
|
)
|
|
litellm.proxy.proxy_server.user_custom_key_generate = None
|
|
|
|
return prisma_client
|
|
|
|
|
|
@pytest.mark.asyncio()
|
|
@pytest.mark.flaky(retries=6, delay=1)
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
async def test_new_user_response(prisma_client):
|
|
try:
|
|
print("prisma client=", prisma_client)
|
|
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
from litellm.proxy.proxy_server import user_api_key_cache
|
|
|
|
_team_id = "ishaan-special-team_{}".format(uuid.uuid4())
|
|
await new_team(
|
|
NewTeamRequest(
|
|
team_id=_team_id,
|
|
),
|
|
http_request=Request(scope={"type": "http"}),
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
|
|
_response = await new_user(
|
|
data=NewUserRequest(
|
|
models=["azure-gpt-3.5"],
|
|
team_id=_team_id,
|
|
tpm_limit=20,
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
)
|
|
print(_response)
|
|
assert _response.models == ["azure-gpt-3.5"]
|
|
assert _response.team_id == _team_id
|
|
assert _response.tpm_limit == 20
|
|
|
|
except Exception as e:
|
|
print("Got Exception", e)
|
|
pytest.fail(f"Got exception {e}")
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"api_route",
|
|
[
|
|
# chat_completion
|
|
APIRoute(path="/engines/{model}/chat/completions", endpoint=chat_completion),
|
|
APIRoute(
|
|
path="/openai/deployments/{model}/chat/completions",
|
|
endpoint=chat_completion,
|
|
),
|
|
APIRoute(path="/chat/completions", endpoint=chat_completion),
|
|
APIRoute(path="/v1/chat/completions", endpoint=chat_completion),
|
|
# completion
|
|
APIRoute(path="/completions", endpoint=completion),
|
|
APIRoute(path="/v1/completions", endpoint=completion),
|
|
APIRoute(path="/engines/{model}/completions", endpoint=completion),
|
|
APIRoute(path="/openai/deployments/{model}/completions", endpoint=completion),
|
|
# embeddings
|
|
APIRoute(path="/v1/embeddings", endpoint=embeddings),
|
|
APIRoute(path="/embeddings", endpoint=embeddings),
|
|
APIRoute(path="/openai/deployments/{model}/embeddings", endpoint=embeddings),
|
|
# image generation
|
|
APIRoute(path="/v1/images/generations", endpoint=image_generation),
|
|
APIRoute(path="/images/generations", endpoint=image_generation),
|
|
# audio transcriptions
|
|
APIRoute(path="/v1/audio/transcriptions", endpoint=audio_transcriptions),
|
|
APIRoute(path="/audio/transcriptions", endpoint=audio_transcriptions),
|
|
# moderations
|
|
APIRoute(path="/v1/moderations", endpoint=moderations),
|
|
APIRoute(path="/moderations", endpoint=moderations),
|
|
# model_list
|
|
APIRoute(path="/v1/models", endpoint=model_list),
|
|
APIRoute(path="/models", endpoint=model_list),
|
|
# threads
|
|
APIRoute(
|
|
path="/v1/threads/thread_49EIN5QF32s4mH20M7GFKdlZ", endpoint=model_list
|
|
),
|
|
],
|
|
ids=lambda route: str(dict(route=route.endpoint.__name__, path=route.path)),
|
|
)
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
def test_generate_and_call_with_valid_key(prisma_client, api_route):
|
|
# 1. Generate a Key, and use it to make a call
|
|
from unittest.mock import MagicMock
|
|
|
|
print("prisma client=", prisma_client)
|
|
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
try:
|
|
|
|
async def test():
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
from litellm.proxy.proxy_server import user_api_key_cache
|
|
|
|
user_api_key_dict = UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
)
|
|
request = NewUserRequest(user_role=LitellmUserRoles.INTERNAL_USER)
|
|
key = await new_user(request, user_api_key_dict=user_api_key_dict)
|
|
print(key)
|
|
user_id = key.user_id
|
|
|
|
# check /user/info to verify user_role was set correctly
|
|
request_mock = MagicMock()
|
|
new_user_info = await user_info(
|
|
request=request_mock,
|
|
user_id=user_id,
|
|
user_api_key_dict=user_api_key_dict,
|
|
)
|
|
new_user_info = new_user_info.user_info
|
|
print("new_user_info=", new_user_info)
|
|
assert new_user_info["user_role"] == LitellmUserRoles.INTERNAL_USER
|
|
assert new_user_info["user_id"] == user_id
|
|
|
|
generated_key = key.key
|
|
bearer_token = "Bearer " + generated_key
|
|
|
|
assert generated_key not in user_api_key_cache.in_memory_cache.cache_dict
|
|
|
|
value_from_prisma = await prisma_client.get_data(
|
|
token=generated_key,
|
|
)
|
|
print("token from prisma", value_from_prisma)
|
|
|
|
request = Request(
|
|
{
|
|
"type": "http",
|
|
"route": api_route,
|
|
"path": api_route.path,
|
|
"headers": [("Authorization", bearer_token)],
|
|
}
|
|
)
|
|
|
|
# use generated key to auth in
|
|
result = await user_api_key_auth(request=request, api_key=bearer_token)
|
|
print("result from user auth with new key", result)
|
|
|
|
asyncio.run(test())
|
|
except Exception as e:
|
|
pytest.fail(f"An exception occurred - {str(e)}")
|
|
|
|
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
def test_call_with_invalid_key(prisma_client):
|
|
# 2. Make a call with invalid key, expect it to fail
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
|
|
async def test():
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
generated_key = "sk-126666"
|
|
bearer_token = "Bearer " + generated_key
|
|
|
|
request = Request(scope={"type": "http"}, receive=None)
|
|
request._url = URL(url="/chat/completions")
|
|
|
|
# use generated key to auth in
|
|
result = await user_api_key_auth(request=request, api_key=bearer_token)
|
|
print("got result", result)
|
|
pytest.fail(f"This should have failed!. IT's an invalid key")
|
|
|
|
with pytest.raises(Exception, match="Authentication Error, Invalid proxy server token passed") as exc_info:
|
|
asyncio.run(test())
|
|
e = exc_info.value
|
|
print("Got Exception", e)
|
|
print(e.message)
|
|
assert "Authentication Error, Invalid proxy server token passed" in e.message
|
|
|
|
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
def test_call_with_invalid_model(prisma_client):
|
|
litellm.set_verbose = True
|
|
# 3. Make a call to a key with an invalid model - expect to fail
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
|
|
async def test():
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
request = NewUserRequest(models=["mistral"])
|
|
key = await new_user(
|
|
data=request,
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
print(key)
|
|
|
|
generated_key = key.key
|
|
bearer_token = "Bearer " + generated_key
|
|
|
|
request = Request(scope={"type": "http"})
|
|
request._url = URL(url="/chat/completions")
|
|
|
|
async def return_body():
|
|
return b'{"model": "gemini-pro-vision"}'
|
|
|
|
request.body = return_body
|
|
|
|
# use generated key to auth in
|
|
print(
|
|
"Bearer token being sent to user_api_key_auth() - {}".format(
|
|
bearer_token
|
|
)
|
|
)
|
|
result = await user_api_key_auth(request=request, api_key=bearer_token)
|
|
pytest.fail(f"This should have failed!. IT's an invalid model")
|
|
|
|
with pytest.raises(ProxyException) as exc_info:
|
|
asyncio.run(test())
|
|
e = exc_info.value
|
|
assert isinstance(e, ProxyException)
|
|
assert e.type == ProxyErrorTypes.key_model_access_denied
|
|
assert e.param == "model"
|
|
|
|
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
def test_call_with_valid_model(prisma_client):
|
|
# 4. Make a call to a key with a valid model - expect to pass
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
try:
|
|
|
|
async def test():
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
request = NewUserRequest(models=["mistral"])
|
|
key = await new_user(
|
|
request,
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
print(key)
|
|
|
|
generated_key = key.key
|
|
bearer_token = "Bearer " + generated_key
|
|
|
|
request = Request(scope={"type": "http"})
|
|
request._url = URL(url="/chat/completions")
|
|
|
|
async def return_body():
|
|
return b'{"model": "mistral"}'
|
|
|
|
request.body = return_body
|
|
|
|
# use generated key to auth in
|
|
result = await user_api_key_auth(request=request, api_key=bearer_token)
|
|
print("result from user auth with new key", result)
|
|
|
|
asyncio.run(test())
|
|
except Exception as e:
|
|
pytest.fail(f"An exception occurred - {str(e)}")
|
|
|
|
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
@pytest.mark.asyncio
|
|
async def test_call_with_valid_model_using_all_models(prisma_client):
|
|
"""
|
|
Do not delete
|
|
this is the Admin UI flow
|
|
1. Create a team with model = `all-proxy-models`
|
|
2. Create a key with model = `all-team-models`
|
|
3. Call /chat/completions with the key -> expect to pass
|
|
"""
|
|
# Make a call to a key with model = `all-proxy-models` this is an Alias from LiteLLM Admin UI
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
try:
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
|
|
team_request = NewTeamRequest(
|
|
team_alias="testing-team",
|
|
models=["all-proxy-models"],
|
|
)
|
|
|
|
new_team_response = await new_team(
|
|
data=team_request,
|
|
user_api_key_dict=UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN),
|
|
http_request=Request(scope={"type": "http"}),
|
|
)
|
|
print("new_team_response", new_team_response)
|
|
created_team_id = new_team_response["team_id"]
|
|
|
|
request = GenerateKeyRequest(
|
|
models=["all-team-models"], team_id=created_team_id
|
|
)
|
|
key = await generate_key_fn(
|
|
data=request,
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
print(key)
|
|
|
|
generated_key = key.key
|
|
bearer_token = "Bearer " + generated_key
|
|
|
|
request = Request(scope={"type": "http"})
|
|
request._url = URL(url="/chat/completions")
|
|
|
|
async def return_body():
|
|
return b'{"model": "mistral"}'
|
|
|
|
request.body = return_body
|
|
|
|
# use generated key to auth in
|
|
result = await user_api_key_auth(request=request, api_key=bearer_token)
|
|
print("result from user auth with new key", result)
|
|
|
|
# call /key/info for key - models == "all-proxy-models"
|
|
key_info = await info_key_fn(
|
|
key=generated_key,
|
|
user_api_key_dict=UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN),
|
|
)
|
|
print("key_info", key_info)
|
|
models = key_info["info"]["models"]
|
|
assert models == ["all-team-models"]
|
|
|
|
except Exception as e:
|
|
pytest.fail(f"An exception occurred - {str(e)}")
|
|
|
|
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
def test_call_with_user_over_budget(prisma_client):
|
|
# 5. Make a call with a key over budget, expect to fail
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
|
|
async def test():
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
request = NewUserRequest(max_budget=0.00001)
|
|
key = await new_user(
|
|
data=request,
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
print(key)
|
|
|
|
generated_key = key.key
|
|
user_id = key.user_id
|
|
bearer_token = "Bearer " + generated_key
|
|
|
|
request = Request(scope={"type": "http"})
|
|
request._url = URL(url="/chat/completions")
|
|
|
|
# use generated key to auth in
|
|
result = await user_api_key_auth(request=request, api_key=bearer_token)
|
|
print("result from user auth with new key", result)
|
|
|
|
# update spend using track_cost callback, make 2nd request, it should fail
|
|
from litellm import Choices, Message, ModelResponse, Usage
|
|
from litellm.proxy.proxy_server import _ProxyDBLogger
|
|
|
|
proxy_db_logger = _ProxyDBLogger()
|
|
|
|
resp = ModelResponse(
|
|
id="chatcmpl-e41836bb-bb8b-4df2-8e70-8f3e160155ac",
|
|
choices=[
|
|
Choices(
|
|
finish_reason=None,
|
|
index=0,
|
|
message=Message(
|
|
content=" Sure! Here is a short poem about the sky:\n\nA canvas of blue, a",
|
|
role="assistant",
|
|
),
|
|
)
|
|
],
|
|
model="gpt-35-turbo", # azure always has model written like this
|
|
usage=Usage(prompt_tokens=210, completion_tokens=200, total_tokens=410),
|
|
)
|
|
await proxy_db_logger._PROXY_track_cost_callback(
|
|
kwargs={
|
|
"stream": False,
|
|
"litellm_params": {
|
|
"metadata": {
|
|
"user_api_key": generated_key,
|
|
"user_api_key_user_id": user_id,
|
|
}
|
|
},
|
|
"response_cost": 0.00002,
|
|
},
|
|
completion_response=resp,
|
|
start_time=datetime.now(),
|
|
end_time=datetime.now(),
|
|
)
|
|
await asyncio.sleep(5)
|
|
# use generated key to auth in
|
|
result = await user_api_key_auth(request=request, api_key=bearer_token)
|
|
print("result from user auth with new key", result)
|
|
pytest.fail("This should have failed!. They key crossed it's budget")
|
|
|
|
with pytest.raises(ProxyException) as exc_info:
|
|
asyncio.run(test())
|
|
e = exc_info.value
|
|
print("got an errror=", e)
|
|
error_detail = e.message
|
|
assert "ExceededBudget:" in error_detail
|
|
assert isinstance(e, ProxyException)
|
|
assert e.type == ProxyErrorTypes.budget_exceeded
|
|
print(vars(e))
|
|
|
|
|
|
def test_end_user_cache_write_unit_test():
|
|
"""
|
|
assert end user object is being written to cache as expected
|
|
"""
|
|
pass
|
|
|
|
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
def test_call_with_end_user_over_budget(prisma_client):
|
|
# Test if a user passed to /chat/completions is tracked & fails when they cross their budget
|
|
# we only check this when litellm.max_end_user_budget is set
|
|
import random
|
|
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
setattr(litellm, "max_end_user_budget", 0.00001)
|
|
|
|
async def test():
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
user = f"ishaan {uuid.uuid4().hex}"
|
|
request = NewCustomerRequest(
|
|
user_id=user, max_budget=0.000001
|
|
) # create a key with no budget
|
|
await new_end_user(
|
|
request,
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
|
|
request = Request(scope={"type": "http"})
|
|
request._url = URL(url="/chat/completions")
|
|
bearer_token = "Bearer sk-1234"
|
|
|
|
async def return_body():
|
|
return_string = f'{{"model": "gemini-pro-vision", "user": "{user}"}}'
|
|
# return string as bytes
|
|
return return_string.encode()
|
|
|
|
request.body = return_body
|
|
|
|
result = await user_api_key_auth(request=request, api_key=bearer_token)
|
|
|
|
# update spend using track_cost callback, make 2nd request, it should fail
|
|
from litellm import Choices, Message, ModelResponse, Usage
|
|
from litellm.proxy.proxy_server import _ProxyDBLogger
|
|
|
|
proxy_db_logger = _ProxyDBLogger()
|
|
|
|
resp = ModelResponse(
|
|
id="chatcmpl-e41836bb-bb8b-4df2-8e70-8f3e160155ac",
|
|
choices=[
|
|
Choices(
|
|
finish_reason=None,
|
|
index=0,
|
|
message=Message(
|
|
content=" Sure! Here is a short poem about the sky:\n\nA canvas of blue, a",
|
|
role="assistant",
|
|
),
|
|
)
|
|
],
|
|
model="gpt-35-turbo", # azure always has model written like this
|
|
usage=Usage(prompt_tokens=210, completion_tokens=200, total_tokens=410),
|
|
)
|
|
await proxy_db_logger._PROXY_track_cost_callback(
|
|
kwargs={
|
|
"stream": False,
|
|
"litellm_params": {
|
|
"metadata": {
|
|
"user_api_key": "sk-1234",
|
|
"user_api_key_end_user_id": user,
|
|
},
|
|
"proxy_server_request": {
|
|
"body": {
|
|
"user": user,
|
|
}
|
|
},
|
|
},
|
|
"response_cost": 10,
|
|
},
|
|
completion_response=resp,
|
|
start_time=datetime.now(),
|
|
end_time=datetime.now(),
|
|
)
|
|
|
|
await asyncio.sleep(10)
|
|
await update_spend(
|
|
prisma_client=prisma_client,
|
|
db_writer_client=None,
|
|
proxy_logging_obj=proxy_logging_obj,
|
|
)
|
|
|
|
# use generated key to auth in
|
|
result = await user_api_key_auth(request=request, api_key=bearer_token)
|
|
print("result from user auth with new key", result)
|
|
pytest.fail("This should have failed!. They key crossed it's budget")
|
|
|
|
with pytest.raises(ProxyException) as exc_info:
|
|
asyncio.run(test())
|
|
e = exc_info.value
|
|
print(f"raised error: {e}, traceback: {traceback.format_exc()}")
|
|
# Handle DataError and other exceptions that don't have .message attribute
|
|
error_detail = getattr(e, "message", str(e))
|
|
assert "ExceededBudget: End User=" in error_detail
|
|
assert "over budget" in error_detail
|
|
assert isinstance(e, ProxyException)
|
|
assert e.type == ProxyErrorTypes.budget_exceeded
|
|
print(vars(e))
|
|
|
|
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
def test_call_with_proxy_over_budget(prisma_client):
|
|
# 5.1 Make a call with a proxy over budget, expect to fail
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
litellm_proxy_budget_name = f"litellm-proxy-budget-{time.time()}"
|
|
setattr(
|
|
litellm.proxy.proxy_server,
|
|
"litellm_proxy_admin_name",
|
|
litellm_proxy_budget_name,
|
|
)
|
|
setattr(litellm, "max_budget", 0.00001)
|
|
from litellm.proxy.proxy_server import user_api_key_cache
|
|
|
|
user_api_key_cache.set_cache(
|
|
key="{}:spend".format(litellm_proxy_budget_name), value=0
|
|
)
|
|
setattr(litellm.proxy.proxy_server, "user_api_key_cache", user_api_key_cache)
|
|
|
|
async def test():
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
request = NewUserRequest()
|
|
key = await new_user(
|
|
data=request,
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
print(key)
|
|
|
|
generated_key = key.key
|
|
user_id = key.user_id
|
|
bearer_token = "Bearer " + generated_key
|
|
|
|
request = Request(scope={"type": "http"})
|
|
request._url = URL(url="/chat/completions")
|
|
|
|
# use generated key to auth in
|
|
result = await user_api_key_auth(request=request, api_key=bearer_token)
|
|
print("result from user auth with new key", result)
|
|
|
|
# update spend using track_cost callback, make 2nd request, it should fail
|
|
from litellm import Choices, Message, ModelResponse, Usage
|
|
from litellm.proxy.proxy_server import _ProxyDBLogger
|
|
|
|
proxy_db_logger = _ProxyDBLogger()
|
|
|
|
resp = ModelResponse(
|
|
id="chatcmpl-e41836bb-bb8b-4df2-8e70-8f3e160155ac",
|
|
choices=[
|
|
Choices(
|
|
finish_reason=None,
|
|
index=0,
|
|
message=Message(
|
|
content=" Sure! Here is a short poem about the sky:\n\nA canvas of blue, a",
|
|
role="assistant",
|
|
),
|
|
)
|
|
],
|
|
model="gpt-35-turbo", # azure always has model written like this
|
|
usage=Usage(prompt_tokens=210, completion_tokens=200, total_tokens=410),
|
|
)
|
|
await proxy_db_logger._PROXY_track_cost_callback(
|
|
kwargs={
|
|
"stream": False,
|
|
"litellm_params": {
|
|
"metadata": {
|
|
"user_api_key": generated_key,
|
|
"user_api_key_user_id": user_id,
|
|
}
|
|
},
|
|
"response_cost": 0.00002,
|
|
},
|
|
completion_response=resp,
|
|
start_time=datetime.now(),
|
|
end_time=datetime.now(),
|
|
)
|
|
|
|
await asyncio.sleep(5)
|
|
# use generated key to auth in
|
|
result = await user_api_key_auth(request=request, api_key=bearer_token)
|
|
print("result from user auth with new key", result)
|
|
pytest.fail(f"This should have failed!. They key crossed it's budget")
|
|
|
|
with pytest.raises(ProxyException) as exc_info:
|
|
asyncio.run(test())
|
|
e = exc_info.value
|
|
if hasattr(e, "message"):
|
|
error_detail = e.message
|
|
else:
|
|
error_detail = traceback.format_exc()
|
|
assert "Budget has been exceeded" in error_detail
|
|
assert isinstance(e, ProxyException)
|
|
assert e.type == ProxyErrorTypes.budget_exceeded
|
|
print(vars(e))
|
|
|
|
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
def test_call_with_user_over_budget_stream(prisma_client):
|
|
# 6. Make a call with a key over budget, expect to fail
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
import logging
|
|
|
|
from litellm._logging import verbose_proxy_logger
|
|
|
|
litellm.set_verbose = True
|
|
verbose_proxy_logger.setLevel(logging.DEBUG)
|
|
|
|
async def test():
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
request = NewUserRequest(max_budget=0.00001)
|
|
key = await new_user(
|
|
data=request,
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
print(key)
|
|
|
|
generated_key = key.key
|
|
user_id = key.user_id
|
|
bearer_token = "Bearer " + generated_key
|
|
|
|
request = Request(scope={"type": "http"})
|
|
request._url = URL(url="/chat/completions")
|
|
|
|
# use generated key to auth in
|
|
result = await user_api_key_auth(request=request, api_key=bearer_token)
|
|
print("result from user auth with new key", result)
|
|
|
|
# update spend using track_cost callback, make 2nd request, it should fail
|
|
from litellm import Choices, Message, ModelResponse, Usage
|
|
from litellm.proxy.proxy_server import _ProxyDBLogger
|
|
|
|
proxy_db_logger = _ProxyDBLogger()
|
|
|
|
resp = ModelResponse(
|
|
id="chatcmpl-e41836bb-bb8b-4df2-8e70-8f3e160155ac",
|
|
choices=[
|
|
Choices(
|
|
finish_reason=None,
|
|
index=0,
|
|
message=Message(
|
|
content=" Sure! Here is a short poem about the sky:\n\nA canvas of blue, a",
|
|
role="assistant",
|
|
),
|
|
)
|
|
],
|
|
model="gpt-35-turbo", # azure always has model written like this
|
|
usage=Usage(prompt_tokens=210, completion_tokens=200, total_tokens=410),
|
|
)
|
|
await proxy_db_logger._PROXY_track_cost_callback(
|
|
kwargs={
|
|
"stream": True,
|
|
"complete_streaming_response": resp,
|
|
"litellm_params": {
|
|
"metadata": {
|
|
"user_api_key": generated_key,
|
|
"user_api_key_user_id": user_id,
|
|
}
|
|
},
|
|
"response_cost": 0.00002,
|
|
},
|
|
completion_response=ModelResponse(),
|
|
start_time=datetime.now(),
|
|
end_time=datetime.now(),
|
|
)
|
|
await asyncio.sleep(5)
|
|
# use generated key to auth in
|
|
result = await user_api_key_auth(request=request, api_key=bearer_token)
|
|
print("result from user auth with new key", result)
|
|
pytest.fail("This should have failed!. They key crossed it's budget")
|
|
|
|
with pytest.raises(ProxyException) as exc_info:
|
|
asyncio.run(test())
|
|
e = exc_info.value
|
|
error_detail = e.message
|
|
assert "ExceededBudget:" in error_detail
|
|
assert isinstance(e, ProxyException)
|
|
assert e.type == ProxyErrorTypes.budget_exceeded
|
|
print(vars(e))
|
|
|
|
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
def test_call_with_proxy_over_budget_stream(prisma_client):
|
|
# 6.1 Make a call with a global proxy over budget, expect to fail
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
litellm_proxy_budget_name = f"litellm-proxy-budget-{time.time()}"
|
|
setattr(
|
|
litellm.proxy.proxy_server,
|
|
"litellm_proxy_admin_name",
|
|
litellm_proxy_budget_name,
|
|
)
|
|
setattr(litellm, "max_budget", 0.00001)
|
|
from litellm.proxy.proxy_server import user_api_key_cache
|
|
|
|
user_api_key_cache.set_cache(
|
|
key="{}:spend".format(litellm_proxy_budget_name), value=0
|
|
)
|
|
setattr(litellm.proxy.proxy_server, "user_api_key_cache", user_api_key_cache)
|
|
|
|
import logging
|
|
|
|
from litellm._logging import verbose_proxy_logger
|
|
|
|
litellm.set_verbose = True
|
|
verbose_proxy_logger.setLevel(logging.DEBUG)
|
|
|
|
async def test():
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
## CREATE PROXY + USER BUDGET ##
|
|
# request = NewUserRequest(
|
|
# max_budget=0.00001, user_id=litellm_proxy_budget_name
|
|
# )
|
|
request = NewUserRequest()
|
|
key = await new_user(
|
|
data=request,
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
print(key)
|
|
|
|
generated_key = key.key
|
|
user_id = key.user_id
|
|
bearer_token = "Bearer " + generated_key
|
|
|
|
request = Request(scope={"type": "http"})
|
|
request._url = URL(url="/chat/completions")
|
|
|
|
# use generated key to auth in
|
|
result = await user_api_key_auth(request=request, api_key=bearer_token)
|
|
print("result from user auth with new key", result)
|
|
|
|
# update spend using track_cost callback, make 2nd request, it should fail
|
|
from litellm import Choices, Message, ModelResponse, Usage
|
|
from litellm.proxy.proxy_server import _ProxyDBLogger
|
|
|
|
proxy_db_logger = _ProxyDBLogger()
|
|
|
|
resp = ModelResponse(
|
|
id="chatcmpl-e41836bb-bb8b-4df2-8e70-8f3e160155ac",
|
|
choices=[
|
|
Choices(
|
|
finish_reason=None,
|
|
index=0,
|
|
message=Message(
|
|
content=" Sure! Here is a short poem about the sky:\n\nA canvas of blue, a",
|
|
role="assistant",
|
|
),
|
|
)
|
|
],
|
|
model="gpt-35-turbo", # azure always has model written like this
|
|
usage=Usage(prompt_tokens=210, completion_tokens=200, total_tokens=410),
|
|
)
|
|
await proxy_db_logger._PROXY_track_cost_callback(
|
|
kwargs={
|
|
"stream": True,
|
|
"complete_streaming_response": resp,
|
|
"litellm_params": {
|
|
"metadata": {
|
|
"user_api_key": generated_key,
|
|
"user_api_key_user_id": user_id,
|
|
}
|
|
},
|
|
"response_cost": 0.00002,
|
|
},
|
|
completion_response=ModelResponse(),
|
|
start_time=datetime.now(),
|
|
end_time=datetime.now(),
|
|
)
|
|
await asyncio.sleep(5)
|
|
# use generated key to auth in
|
|
result = await user_api_key_auth(request=request, api_key=bearer_token)
|
|
print("result from user auth with new key", result)
|
|
pytest.fail(f"This should have failed!. They key crossed it's budget")
|
|
|
|
with pytest.raises(Exception, match="Budget has been exceeded") as exc_info:
|
|
asyncio.run(test())
|
|
e = exc_info.value
|
|
error_detail = e.message
|
|
assert "Budget has been exceeded" in error_detail
|
|
print(vars(e))
|
|
|
|
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
def test_generate_and_call_with_valid_key_never_expires(prisma_client):
|
|
# 7. Make a call with an key that never expires, expect to pass
|
|
|
|
print("prisma client=", prisma_client)
|
|
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
try:
|
|
|
|
async def test():
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
request = NewUserRequest(duration=None)
|
|
key = await new_user(
|
|
data=request,
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
print(key)
|
|
|
|
generated_key = key.key
|
|
bearer_token = "Bearer " + generated_key
|
|
|
|
request = Request(scope={"type": "http"})
|
|
request._url = URL(url="/chat/completions")
|
|
|
|
# use generated key to auth in
|
|
result = await user_api_key_auth(request=request, api_key=bearer_token)
|
|
print("result from user auth with new key", result)
|
|
|
|
asyncio.run(test())
|
|
except Exception as e:
|
|
pytest.fail(f"An exception occurred - {str(e)}")
|
|
|
|
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
def test_generate_and_call_with_expired_key(prisma_client):
|
|
# 8. Make a call with an expired key, expect to fail
|
|
|
|
print("prisma client=", prisma_client)
|
|
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
|
|
async def test():
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
request = NewUserRequest(duration="0s")
|
|
key = await new_user(
|
|
data=request,
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
print(key)
|
|
|
|
generated_key = key.key
|
|
bearer_token = "Bearer " + generated_key
|
|
|
|
request = Request(scope={"type": "http"})
|
|
request._url = URL(url="/chat/completions")
|
|
|
|
# use generated key to auth in
|
|
result = await user_api_key_auth(request=request, api_key=bearer_token)
|
|
print("result from user auth with new key", result)
|
|
pytest.fail("This should have failed!. It's an expired key")
|
|
|
|
with pytest.raises(Exception, match="Authentication Error") as exc_info:
|
|
asyncio.run(test())
|
|
e = exc_info.value
|
|
print("Got Exception", e)
|
|
print(e.message)
|
|
assert "Authentication Error" in e.message
|
|
assert e.type == ProxyErrorTypes.expired_key
|
|
|
|
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
def test_delete_key(prisma_client):
|
|
# 9. Generate a Key, delete it. Check if deletion works fine
|
|
|
|
print("prisma client=", prisma_client)
|
|
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
setattr(litellm.proxy.proxy_server, "user_custom_auth", None)
|
|
try:
|
|
|
|
async def test():
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
from litellm.proxy.proxy_server import user_api_key_cache
|
|
|
|
request = NewUserRequest()
|
|
key = await new_user(
|
|
data=request,
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
print(key)
|
|
|
|
generated_key = key.key
|
|
bearer_token = "Bearer " + generated_key
|
|
|
|
delete_key_request = KeyRequest(keys=[generated_key])
|
|
|
|
bearer_token = "Bearer sk-1234"
|
|
|
|
request = Request(scope={"type": "http"})
|
|
request._url = URL(url="/key/delete")
|
|
|
|
# use generated key to auth in
|
|
result = await user_api_key_auth(request=request, api_key=bearer_token)
|
|
print(f"result: {result}")
|
|
result.user_role = LitellmUserRoles.PROXY_ADMIN
|
|
# delete the key
|
|
result_delete_key = await delete_key_fn(
|
|
data=delete_key_request, user_api_key_dict=result
|
|
)
|
|
print("result from delete key", result_delete_key)
|
|
assert result_delete_key == {"deleted_keys": [generated_key]}
|
|
|
|
assert generated_key not in user_api_key_cache.in_memory_cache.cache_dict
|
|
assert (
|
|
hash_token(generated_key)
|
|
not in user_api_key_cache.in_memory_cache.cache_dict
|
|
)
|
|
|
|
asyncio.run(test())
|
|
except Exception as e:
|
|
pytest.fail(f"An exception occurred - {str(e)}")
|
|
|
|
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
def test_delete_key_auth(prisma_client):
|
|
# 10. Generate a Key, delete it, use it to make a call -> expect fail
|
|
|
|
print("prisma client=", prisma_client)
|
|
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
try:
|
|
|
|
async def test():
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
from litellm.proxy.proxy_server import user_api_key_cache
|
|
|
|
request = NewUserRequest()
|
|
key = await new_user(
|
|
data=request,
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
print(key)
|
|
|
|
generated_key = key.key
|
|
bearer_token = "Bearer " + generated_key
|
|
|
|
delete_key_request = KeyRequest(keys=[generated_key])
|
|
|
|
# delete the key
|
|
bearer_token = "Bearer sk-1234"
|
|
|
|
request = Request(scope={"type": "http"})
|
|
request._url = URL(url="/key/delete")
|
|
|
|
# use generated key to auth in
|
|
result = await user_api_key_auth(request=request, api_key=bearer_token)
|
|
print(f"result: {result}")
|
|
result.user_role = LitellmUserRoles.PROXY_ADMIN
|
|
|
|
result_delete_key = await delete_key_fn(
|
|
data=delete_key_request, user_api_key_dict=result
|
|
)
|
|
|
|
print("result from delete key", result_delete_key)
|
|
assert result_delete_key == {"deleted_keys": [generated_key]}
|
|
|
|
request = Request(scope={"type": "http"}, receive=None)
|
|
request._url = URL(url="/chat/completions")
|
|
|
|
assert generated_key not in user_api_key_cache.in_memory_cache.cache_dict
|
|
assert (
|
|
hash_token(generated_key)
|
|
not in user_api_key_cache.in_memory_cache.cache_dict
|
|
)
|
|
|
|
# use generated key to auth in
|
|
bearer_token = "Bearer " + generated_key
|
|
result = await user_api_key_auth(request=request, api_key=bearer_token)
|
|
print("got result", result)
|
|
pytest.fail(f"This should have failed!. IT's an invalid key")
|
|
|
|
asyncio.run(test())
|
|
except Exception as e:
|
|
print("Got Exception", e)
|
|
# Handle different exception types - ProxyException has .message, others might have .detail or str(e)
|
|
error_message = (
|
|
getattr(e, "message", None) or getattr(e, "detail", None) or str(e)
|
|
)
|
|
print(f"Error message: {error_message}")
|
|
assert (
|
|
"Authentication Error" in error_message
|
|
or "Invalid proxy server token" in error_message
|
|
or "not found in db" in error_message
|
|
)
|
|
pass
|
|
|
|
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
def test_generate_and_call_key_info(prisma_client):
|
|
# 10. Generate a Key, cal key/info
|
|
|
|
print("prisma client=", prisma_client)
|
|
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
try:
|
|
|
|
async def test():
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
request = NewUserRequest(
|
|
metadata={"team": "litellm-team3", "project": "litellm-project3"}
|
|
)
|
|
key = await new_user(
|
|
data=request,
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
print(key)
|
|
|
|
generated_key = key.key
|
|
|
|
# use generated key to auth in
|
|
result = await info_key_fn(
|
|
key=generated_key,
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
),
|
|
)
|
|
print("result from info_key_fn", result)
|
|
assert result["key"] == generated_key
|
|
print("\n info for key=", result["info"])
|
|
assert result["info"]["max_parallel_requests"] == None
|
|
assert result["info"]["metadata"] == {
|
|
"team": "litellm-team3",
|
|
"project": "litellm-project3",
|
|
}
|
|
|
|
# cleanup - delete key
|
|
delete_key_request = KeyRequest(keys=[generated_key])
|
|
bearer_token = "Bearer sk-1234"
|
|
|
|
request = Request(scope={"type": "http"})
|
|
request._url = URL(url="/key/delete")
|
|
|
|
# use generated key to auth in
|
|
result = await user_api_key_auth(request=request, api_key=bearer_token)
|
|
print(f"result: {result}")
|
|
result.user_role = LitellmUserRoles.PROXY_ADMIN
|
|
|
|
result_delete_key = await delete_key_fn(
|
|
data=delete_key_request, user_api_key_dict=result
|
|
)
|
|
|
|
asyncio.run(test())
|
|
except Exception as e:
|
|
pytest.fail(f"An exception occurred - {str(e)}")
|
|
|
|
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
def test_generate_and_update_key(prisma_client):
|
|
# 11. Generate a Key, cal key/info, call key/update, call key/info
|
|
# Check if data gets updated
|
|
# Check if untouched data does not get updated
|
|
from litellm._uuid import uuid
|
|
|
|
print("prisma client=", prisma_client)
|
|
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
try:
|
|
|
|
async def test():
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
|
|
# create team "litellm-core-infra@gmail.com""
|
|
print("creating team litellm-core-infra@gmail.com")
|
|
_team_1 = "litellm-core-infra@gmail.com_{}".format(uuid.uuid4())
|
|
await new_team(
|
|
NewTeamRequest(
|
|
team_id=_team_1,
|
|
),
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
http_request=Request(scope={"type": "http"}),
|
|
)
|
|
|
|
_team_2 = "ishaan-special-team_{}".format(uuid.uuid4())
|
|
await new_team(
|
|
NewTeamRequest(
|
|
team_id=_team_2,
|
|
),
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
http_request=Request(scope={"type": "http"}),
|
|
)
|
|
|
|
request = NewUserRequest(
|
|
metadata={"project": "litellm-project3"},
|
|
team_id=_team_1,
|
|
)
|
|
|
|
key = await new_user(
|
|
data=request,
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
print(key)
|
|
|
|
generated_key = key.key
|
|
|
|
# use generated key to auth in
|
|
result = await info_key_fn(
|
|
key=generated_key,
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
),
|
|
)
|
|
print("result from info_key_fn", result)
|
|
assert result["key"] == generated_key
|
|
print("\n info for key=", result["info"])
|
|
assert result["info"]["max_parallel_requests"] == None
|
|
assert result["info"]["metadata"] == {
|
|
"project": "litellm-project3",
|
|
}
|
|
assert result["info"]["team_id"] == _team_1
|
|
|
|
request = Request(scope={"type": "http"})
|
|
request._url = URL(url="/update/key")
|
|
|
|
# update the key
|
|
response1 = await update_key_fn(
|
|
request=Request,
|
|
data=UpdateKeyRequest(
|
|
key=generated_key,
|
|
models=["ada", "babbage", "curie", "davinci"],
|
|
budget_duration="1mo",
|
|
max_budget=100,
|
|
),
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
|
|
print("response1=", response1)
|
|
|
|
# update the tpm limit
|
|
response2 = await update_key_fn(
|
|
request=Request,
|
|
data=UpdateKeyRequest(key=generated_key, tpm_limit=1000),
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
print("response2=", response2)
|
|
|
|
# get info on key after update
|
|
result = await info_key_fn(
|
|
key=generated_key,
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
),
|
|
)
|
|
print("result from info_key_fn", result)
|
|
assert result["key"] == generated_key
|
|
print("\n info for key=", result["info"])
|
|
assert result["info"]["max_parallel_requests"] == None
|
|
assert result["info"]["metadata"] == {
|
|
"project": "litellm-project3",
|
|
}
|
|
assert result["info"]["models"] == ["ada", "babbage", "curie", "davinci"]
|
|
assert result["info"]["tpm_limit"] == 1000
|
|
assert result["info"]["budget_duration"] == "1mo"
|
|
assert result["info"]["max_budget"] == 100
|
|
|
|
# budget_reset_at should exist for "1mo" duration
|
|
assert result["info"]["budget_reset_at"] is not None
|
|
budget_reset_at = result["info"]["budget_reset_at"].replace(
|
|
tzinfo=timezone.utc
|
|
)
|
|
current_time = datetime.now(timezone.utc)
|
|
|
|
print(f"Budget reset time: {budget_reset_at}")
|
|
print(f"Current time: {current_time}")
|
|
|
|
# Instead of checking exact timing, just verify that:
|
|
# 1. Both are in the same day (for tests running same day)
|
|
# 2. Or budget_reset_at is in next month
|
|
if budget_reset_at.day == current_time.day:
|
|
# Same day of month - just check month difference
|
|
month_diff = budget_reset_at.month - current_time.month
|
|
if budget_reset_at.year > current_time.year:
|
|
month_diff += 12
|
|
|
|
# Should be scheduled for next month (at least 0.5 month away)
|
|
assert (
|
|
month_diff >= 1
|
|
), f"Expected reset to be at least 1 month ahead, got {month_diff} months"
|
|
assert (
|
|
month_diff <= 2
|
|
), f"Expected reset to be at most 2 months ahead, got {month_diff} months"
|
|
else:
|
|
# Just ensure the date is reasonable (not more than 40 days away)
|
|
days_diff = (budget_reset_at - current_time).days
|
|
assert (
|
|
0 <= days_diff <= 40
|
|
), f"Expected reset date to be reasonable, got {days_diff} days from now"
|
|
|
|
# cleanup - delete key
|
|
delete_key_request = KeyRequest(keys=[generated_key])
|
|
|
|
# delete the key
|
|
bearer_token = "Bearer sk-1234"
|
|
|
|
request = Request(scope={"type": "http"})
|
|
request._url = URL(url="/key/delete")
|
|
|
|
# use generated key to auth in
|
|
result = await user_api_key_auth(request=request, api_key=bearer_token)
|
|
print(f"result: {result}")
|
|
result.user_role = LitellmUserRoles.PROXY_ADMIN
|
|
|
|
result_delete_key = await delete_key_fn(
|
|
data=delete_key_request, user_api_key_dict=result
|
|
)
|
|
|
|
asyncio.run(test())
|
|
except Exception as e:
|
|
print("Got Exception", e)
|
|
pytest.fail(f"An exception occurred - {str(e)}\n{traceback.format_exc()}")
|
|
|
|
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
def test_key_generate_with_custom_auth(prisma_client):
|
|
# custom - generate key function
|
|
async def custom_generate_key_fn(data: GenerateKeyRequest) -> dict:
|
|
"""
|
|
Asynchronous function for generating a key based on the input data.
|
|
|
|
Args:
|
|
data (GenerateKeyRequest): The input data for key generation.
|
|
|
|
Returns:
|
|
dict: A dictionary containing the decision and an optional message.
|
|
{
|
|
"decision": False,
|
|
"message": "This violates LiteLLM Proxy Rules. No team id provided.",
|
|
}
|
|
"""
|
|
|
|
# decide if a key should be generated or not
|
|
print("using custom auth function!")
|
|
data_json = data.json() # type: ignore
|
|
|
|
# Unpacking variables
|
|
team_id = data_json.get("team_id")
|
|
duration = data_json.get("duration")
|
|
models = data_json.get("models")
|
|
aliases = data_json.get("aliases")
|
|
config = data_json.get("config")
|
|
spend = data_json.get("spend")
|
|
user_id = data_json.get("user_id")
|
|
max_parallel_requests = data_json.get("max_parallel_requests")
|
|
metadata = data_json.get("metadata")
|
|
tpm_limit = data_json.get("tpm_limit")
|
|
rpm_limit = data_json.get("rpm_limit")
|
|
|
|
if team_id is not None and team_id == "litellm-core-infra@gmail.com":
|
|
# only team_id="litellm-core-infra@gmail.com" can make keys
|
|
return {
|
|
"decision": True,
|
|
}
|
|
else:
|
|
print("Failed custom auth")
|
|
return {
|
|
"decision": False,
|
|
"message": "This violates LiteLLM Proxy Rules. No team id provided.",
|
|
}
|
|
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
setattr(
|
|
litellm.proxy.proxy_server, "user_custom_key_generate", custom_generate_key_fn
|
|
)
|
|
try:
|
|
|
|
async def test():
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
request = GenerateKeyRequest()
|
|
|
|
with pytest.raises(
|
|
Exception, match=re.escape("This violates LiteLLM Proxy Rules. No team id provided.")
|
|
) as exc_info:
|
|
key = await generate_key_fn(
|
|
request,
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
e = exc_info.value
|
|
print("Got Exception", e)
|
|
print(e.message)
|
|
print("First request failed!. This is expected")
|
|
assert (
|
|
"This violates LiteLLM Proxy Rules. No team id provided."
|
|
in e.message
|
|
)
|
|
|
|
request_2 = GenerateKeyRequest(
|
|
team_id="litellm-core-infra@gmail.com",
|
|
)
|
|
|
|
key = await generate_key_fn(
|
|
request_2,
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
print(key)
|
|
generated_key = key.key
|
|
|
|
asyncio.run(test())
|
|
except Exception as e:
|
|
print("Got Exception", e)
|
|
if hasattr(e, "message"):
|
|
print(e.message)
|
|
else:
|
|
print(e)
|
|
pytest.fail(f"An exception occurred - {str(e)}")
|
|
|
|
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
def test_call_with_key_over_budget(prisma_client):
|
|
# 12. Make a call with a key over budget, expect to fail
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
|
|
async def test():
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
request = GenerateKeyRequest(max_budget=0.00001)
|
|
key = await generate_key_fn(
|
|
request,
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
print(key)
|
|
|
|
generated_key = key.key
|
|
user_id = key.user_id
|
|
bearer_token = "Bearer " + generated_key
|
|
|
|
request = Request(scope={"type": "http"})
|
|
request._url = URL(url="/chat/completions")
|
|
|
|
# use generated key to auth in
|
|
result = await user_api_key_auth(request=request, api_key=bearer_token)
|
|
print("result from user auth with new key", result)
|
|
|
|
# update spend using track_cost callback, make 2nd request, it should fail
|
|
from litellm import Choices, Message, ModelResponse, Usage
|
|
from litellm.caching.caching import Cache
|
|
from litellm.proxy.proxy_server import _ProxyDBLogger
|
|
|
|
proxy_db_logger = _ProxyDBLogger()
|
|
|
|
litellm.cache = Cache()
|
|
import time
|
|
from litellm._uuid import uuid
|
|
|
|
request_id = f"chatcmpl-e41836bb-bb8b-4df2-8e70-8f3e160155ac{uuid.uuid4()}"
|
|
|
|
resp = ModelResponse(
|
|
id=request_id,
|
|
choices=[
|
|
Choices(
|
|
finish_reason=None,
|
|
index=0,
|
|
message=Message(
|
|
content=" Sure! Here is a short poem about the sky:\n\nA canvas of blue, a",
|
|
role="assistant",
|
|
),
|
|
)
|
|
],
|
|
model="gpt-35-turbo", # azure always has model written like this
|
|
usage=Usage(prompt_tokens=210, completion_tokens=200, total_tokens=410),
|
|
)
|
|
await proxy_db_logger._PROXY_track_cost_callback(
|
|
kwargs={
|
|
"model": "chatgpt-v-3",
|
|
"stream": False,
|
|
"litellm_params": {
|
|
"metadata": {
|
|
"user_api_key": hash_token(generated_key),
|
|
"user_api_key_user_id": user_id,
|
|
}
|
|
},
|
|
"response_cost": 0.00002,
|
|
},
|
|
completion_response=resp,
|
|
start_time=datetime.now(),
|
|
end_time=datetime.now(),
|
|
)
|
|
await update_spend(
|
|
prisma_client=prisma_client,
|
|
db_writer_client=None,
|
|
proxy_logging_obj=proxy_logging_obj,
|
|
)
|
|
# test spend_log was written and we can read it
|
|
spend_logs = await view_spend_logs(
|
|
request_id=request_id,
|
|
user_api_key_dict=UserAPIKeyAuth(api_key=generated_key),
|
|
)
|
|
|
|
print("read spend logs", spend_logs)
|
|
assert len(spend_logs) == 1
|
|
|
|
spend_log = spend_logs[0]
|
|
|
|
assert spend_log.request_id == request_id
|
|
assert spend_log.spend == float("2e-05")
|
|
assert spend_log.model == "chatgpt-v-3"
|
|
assert (
|
|
spend_log.cache_key
|
|
== "509ba0554a7129ae4f4fd13d11c141acce5549bb6aaf1f629ed543101615658e"
|
|
)
|
|
|
|
# use generated key to auth in
|
|
result = await user_api_key_auth(request=request, api_key=bearer_token)
|
|
print("result from user auth with new key", result)
|
|
pytest.fail("This should have failed!. They key crossed it's budget")
|
|
|
|
with pytest.raises(ProxyException) as exc_info:
|
|
asyncio.run(test())
|
|
e = exc_info.value
|
|
traceback.print_exc()
|
|
if hasattr(e, "message"):
|
|
error_detail = e.message
|
|
else:
|
|
error_detail = str(e)
|
|
assert "Budget has been exceeded" in error_detail
|
|
assert isinstance(e, ProxyException)
|
|
assert e.type == ProxyErrorTypes.budget_exceeded
|
|
print(vars(e))
|
|
|
|
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
def test_call_with_key_over_budget_no_cache(prisma_client):
|
|
# 12. Make a call with a key over budget, expect to fail
|
|
# ✅ Tests if spend trackign works when the key does not exist in memory
|
|
# Related to this: https://github.com/BerriAI/litellm/issues/3920
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
|
|
async def test():
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
request = GenerateKeyRequest(max_budget=0.00001)
|
|
key = await generate_key_fn(
|
|
request,
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
print(key)
|
|
|
|
generated_key = key.key
|
|
user_id = key.user_id
|
|
bearer_token = "Bearer " + generated_key
|
|
|
|
request = Request(scope={"type": "http"})
|
|
request._url = URL(url="/chat/completions")
|
|
|
|
# use generated key to auth in
|
|
result = await user_api_key_auth(request=request, api_key=bearer_token)
|
|
print("result from user auth with new key", result)
|
|
|
|
# update spend using track_cost callback, make 2nd request, it should fail
|
|
from litellm.proxy.proxy_server import _ProxyDBLogger
|
|
from litellm.proxy.proxy_server import user_api_key_cache
|
|
|
|
user_api_key_cache.in_memory_cache.cache_dict = {}
|
|
setattr(litellm.proxy.proxy_server, "proxy_batch_write_at", 1)
|
|
|
|
from litellm import Choices, Message, ModelResponse, Usage
|
|
from litellm.caching.caching import Cache
|
|
|
|
litellm.cache = Cache()
|
|
import time
|
|
from litellm._uuid import uuid
|
|
|
|
request_id = f"chatcmpl-e41836bb-bb8b-4df2-8e70-8f3e160155ac{uuid.uuid4()}"
|
|
|
|
resp = ModelResponse(
|
|
id=request_id,
|
|
choices=[
|
|
Choices(
|
|
finish_reason=None,
|
|
index=0,
|
|
message=Message(
|
|
content=" Sure! Here is a short poem about the sky:\n\nA canvas of blue, a",
|
|
role="assistant",
|
|
),
|
|
)
|
|
],
|
|
model="gpt-35-turbo", # azure always has model written like this
|
|
usage=Usage(prompt_tokens=210, completion_tokens=200, total_tokens=410),
|
|
)
|
|
proxy_db_logger = _ProxyDBLogger()
|
|
await proxy_db_logger._PROXY_track_cost_callback(
|
|
kwargs={
|
|
"model": "chatgpt-v-3",
|
|
"stream": False,
|
|
"litellm_params": {
|
|
"metadata": {
|
|
"user_api_key": hash_token(generated_key),
|
|
"user_api_key_user_id": user_id,
|
|
}
|
|
},
|
|
"response_cost": 0.00002,
|
|
},
|
|
completion_response=resp,
|
|
start_time=datetime.now(),
|
|
end_time=datetime.now(),
|
|
)
|
|
await asyncio.sleep(10)
|
|
await update_spend(
|
|
prisma_client=prisma_client,
|
|
db_writer_client=None,
|
|
proxy_logging_obj=proxy_logging_obj,
|
|
)
|
|
# test spend_log was written and we can read it
|
|
spend_logs = await view_spend_logs(
|
|
request_id=request_id,
|
|
user_api_key_dict=UserAPIKeyAuth(api_key=generated_key),
|
|
)
|
|
|
|
print("read spend logs", spend_logs)
|
|
assert len(spend_logs) == 1
|
|
|
|
spend_log = spend_logs[0]
|
|
|
|
assert spend_log.request_id == request_id
|
|
assert spend_log.spend == float("2e-05")
|
|
assert spend_log.model == "chatgpt-v-3"
|
|
assert (
|
|
spend_log.cache_key
|
|
== "509ba0554a7129ae4f4fd13d11c141acce5549bb6aaf1f629ed543101615658e"
|
|
)
|
|
|
|
# use generated key to auth in
|
|
result = await user_api_key_auth(request=request, api_key=bearer_token)
|
|
print("result from user auth with new key", result)
|
|
pytest.fail(f"This should have failed!. They key crossed it's budget")
|
|
|
|
with pytest.raises(ProxyException) as exc_info:
|
|
asyncio.run(test())
|
|
e = exc_info.value
|
|
traceback.print_exc()
|
|
if hasattr(e, "message"):
|
|
error_detail = e.message
|
|
else:
|
|
error_detail = str(e)
|
|
assert "Budget has been exceeded" in error_detail
|
|
assert isinstance(e, ProxyException)
|
|
assert e.type == ProxyErrorTypes.budget_exceeded
|
|
print(vars(e))
|
|
|
|
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
@pytest.mark.asyncio()
|
|
@pytest.mark.parametrize(
|
|
"request_model,should_pass",
|
|
[
|
|
("openai/gpt-4o-mini", False),
|
|
("gpt-4o-mini", False),
|
|
("gpt-4o", True),
|
|
],
|
|
)
|
|
@pytest.mark.flaky(retries=3, delay=2)
|
|
async def test_aasync_call_with_key_over_model_budget(
|
|
prisma_client, request_model, should_pass
|
|
):
|
|
# 12. Make a call with a key over budget, expect to fail
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
setattr(litellm.proxy.proxy_server, "premium_user", True)
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
verbose_proxy_logger.setLevel(logging.DEBUG)
|
|
|
|
# Use the proxy server's existing budget limiter instead of creating a new one
|
|
# This ensures the budget limiter's cache is shared between the callback and auth checks
|
|
from litellm.proxy.proxy_server import model_max_budget_limiter
|
|
|
|
# set budget for chatgpt-v-3 to 0.000001, expect the next request to fail
|
|
model_max_budget = {
|
|
"gpt-4o-mini": {
|
|
"budget_limit": "0.000001",
|
|
"time_period": "1d",
|
|
},
|
|
"gpt-4o": {
|
|
"budget_limit": "200",
|
|
"time_period": "30d",
|
|
},
|
|
}
|
|
|
|
request = GenerateKeyRequest(
|
|
max_budget=100000, # the key itself has a very high budget
|
|
model_max_budget=model_max_budget,
|
|
)
|
|
key = await generate_key_fn(
|
|
request,
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
print(key)
|
|
|
|
generated_key = key.key
|
|
user_id = key.user_id
|
|
bearer_token = "Bearer " + generated_key
|
|
|
|
request = Request(scope={"type": "http"})
|
|
request._url = URL(url="/chat/completions")
|
|
|
|
async def return_body():
|
|
request_str = f'{{"model": "{request_model}"}}' # Added extra curly braces to escape JSON
|
|
return request_str.encode()
|
|
|
|
request.body = return_body
|
|
|
|
# use generated key to auth in
|
|
result = await user_api_key_auth(request=request, api_key=bearer_token)
|
|
print("result from user auth with new key", result)
|
|
|
|
# update spend using track_cost callback, make 2nd request, it should fail
|
|
response = await litellm.acompletion(
|
|
model=request_model,
|
|
messages=[{"role": "user", "content": "Hello, how are you?"}],
|
|
metadata={
|
|
"user_api_key": hash_token(generated_key),
|
|
"user_api_key_model_max_budget": model_max_budget,
|
|
},
|
|
)
|
|
|
|
# Manually trigger the budget limiter callback to avoid event loop issues with logging worker
|
|
# This ensures the spend is tracked immediately without relying on async background tasks
|
|
import time
|
|
|
|
# Create a mock kwargs object that the callback expects (StandardLoggingPayload is a TypedDict, so use dict)
|
|
mock_kwargs = {
|
|
"standard_logging_object": {
|
|
"response_cost": getattr(response, "_hidden_params", {}).get(
|
|
"response_cost", 0.0001
|
|
), # Use actual cost or small fallback
|
|
"model": request_model,
|
|
"metadata": {
|
|
"user_api_key_hash": hash_token(generated_key),
|
|
},
|
|
},
|
|
"litellm_params": {
|
|
"metadata": {
|
|
"user_api_key": hash_token(generated_key),
|
|
"user_api_key_model_max_budget": model_max_budget,
|
|
}
|
|
},
|
|
}
|
|
|
|
# Call the budget limiter callback directly to ensure spend is recorded
|
|
await model_max_budget_limiter.async_log_success_event(
|
|
kwargs=mock_kwargs,
|
|
response_obj=response,
|
|
start_time=time.time(),
|
|
end_time=time.time(),
|
|
)
|
|
|
|
# Small delay to ensure cache write completes
|
|
await asyncio.sleep(0.5)
|
|
|
|
# use generated key to auth in
|
|
if should_pass:
|
|
result = await user_api_key_auth(request=request, api_key=bearer_token)
|
|
print(
|
|
f"Passed request for model={request_model}, model_max_budget={model_max_budget}"
|
|
)
|
|
print("result from user auth with new key", result)
|
|
return
|
|
|
|
with pytest.raises(ProxyException) as exc_info:
|
|
await user_api_key_auth(request=request, api_key=bearer_token)
|
|
assert f"exceeded budget for model={request_model}" in exc_info.value.message
|
|
assert exc_info.value.type == ProxyErrorTypes.budget_exceeded
|
|
|
|
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
@pytest.mark.asyncio()
|
|
async def test_call_with_key_never_over_budget(prisma_client):
|
|
# Make a call with a key with budget=None, it should never fail
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
try:
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
request = GenerateKeyRequest(max_budget=None)
|
|
key = await generate_key_fn(
|
|
request,
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
print(key)
|
|
|
|
generated_key = key.key
|
|
user_id = key.user_id
|
|
bearer_token = "Bearer " + generated_key
|
|
|
|
request = Request(scope={"type": "http"})
|
|
request._url = URL(url="/chat/completions")
|
|
|
|
# use generated key to auth in
|
|
result = await user_api_key_auth(request=request, api_key=bearer_token)
|
|
print("result from user auth with new key: {result}")
|
|
|
|
# update spend using track_cost callback, make 2nd request, it should fail
|
|
import time
|
|
from litellm._uuid import uuid
|
|
|
|
from litellm import Choices, Message, ModelResponse, Usage
|
|
from litellm.proxy.proxy_server import _ProxyDBLogger
|
|
|
|
proxy_db_logger = _ProxyDBLogger()
|
|
|
|
request_id = f"chatcmpl-{uuid.uuid4()}"
|
|
|
|
resp = ModelResponse(
|
|
id=request_id,
|
|
choices=[
|
|
Choices(
|
|
finish_reason=None,
|
|
index=0,
|
|
message=Message(
|
|
content=" Sure! Here is a short poem about the sky:\n\nA canvas of blue, a",
|
|
role="assistant",
|
|
),
|
|
)
|
|
],
|
|
model="gpt-35-turbo", # azure always has model written like this
|
|
usage=Usage(
|
|
prompt_tokens=210000, completion_tokens=200000, total_tokens=41000
|
|
),
|
|
)
|
|
await proxy_db_logger._PROXY_track_cost_callback(
|
|
kwargs={
|
|
"model": "chatgpt-v-3",
|
|
"stream": False,
|
|
"litellm_params": {
|
|
"metadata": {
|
|
"user_api_key": hash_token(generated_key),
|
|
"user_api_key_user_id": user_id,
|
|
}
|
|
},
|
|
"response_cost": 200000,
|
|
},
|
|
completion_response=resp,
|
|
start_time=datetime.now(),
|
|
end_time=datetime.now(),
|
|
)
|
|
await update_spend(
|
|
prisma_client=prisma_client,
|
|
db_writer_client=None,
|
|
proxy_logging_obj=proxy_logging_obj,
|
|
)
|
|
# use generated key to auth in
|
|
result = await user_api_key_auth(request=request, api_key=bearer_token)
|
|
print("result from user auth with new key", result)
|
|
except Exception as e:
|
|
pytest.fail(f"This should have not failed!. They key uses max_budget=None. {e}")
|
|
|
|
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
@pytest.mark.asyncio
|
|
async def test_call_with_key_over_budget_stream(prisma_client):
|
|
# 14. Make a call with a key over budget, expect to fail
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
import logging
|
|
|
|
from litellm._logging import verbose_proxy_logger
|
|
|
|
litellm.set_verbose = True
|
|
verbose_proxy_logger.setLevel(logging.DEBUG)
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
request = GenerateKeyRequest(max_budget=0.00001)
|
|
key = await generate_key_fn(
|
|
request,
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
print(key)
|
|
|
|
generated_key = key.key
|
|
user_id = key.user_id
|
|
bearer_token = "Bearer " + generated_key
|
|
print(f"generated_key: {generated_key}")
|
|
request = Request(scope={"type": "http"})
|
|
request._url = URL(url="/chat/completions")
|
|
|
|
# use generated key to auth in
|
|
result = await user_api_key_auth(request=request, api_key=bearer_token)
|
|
print("result from user auth with new key", result)
|
|
|
|
# update spend using track_cost callback, make 2nd request, it should fail
|
|
import time
|
|
from litellm._uuid import uuid
|
|
|
|
from litellm import Choices, Message, ModelResponse, Usage
|
|
from litellm.proxy.proxy_server import _ProxyDBLogger
|
|
|
|
proxy_db_logger = _ProxyDBLogger()
|
|
|
|
request_id = f"chatcmpl-e41836bb-bb8b-4df2-8e70-8f3e160155ac{uuid.uuid4()}"
|
|
resp = ModelResponse(
|
|
id=request_id,
|
|
choices=[
|
|
Choices(
|
|
finish_reason=None,
|
|
index=0,
|
|
message=Message(
|
|
content=" Sure! Here is a short poem about the sky:\n\nA canvas of blue, a",
|
|
role="assistant",
|
|
),
|
|
)
|
|
],
|
|
model="gpt-35-turbo", # azure always has model written like this
|
|
usage=Usage(prompt_tokens=210, completion_tokens=200, total_tokens=410),
|
|
)
|
|
await proxy_db_logger._PROXY_track_cost_callback(
|
|
kwargs={
|
|
"call_type": "acompletion",
|
|
"model": "sagemaker-chatgpt-v-3",
|
|
"stream": True,
|
|
"complete_streaming_response": resp,
|
|
"litellm_params": {
|
|
"metadata": {
|
|
"user_api_key": hash_token(generated_key),
|
|
"user_api_key_user_id": user_id,
|
|
}
|
|
},
|
|
"response_cost": 0.00005,
|
|
},
|
|
completion_response=resp,
|
|
start_time=datetime.now(),
|
|
end_time=datetime.now(),
|
|
)
|
|
await update_spend(
|
|
prisma_client=prisma_client,
|
|
db_writer_client=None,
|
|
proxy_logging_obj=proxy_logging_obj,
|
|
)
|
|
# use generated key to auth in
|
|
with pytest.raises(Exception, match="Budget has been exceeded") as exc_info:
|
|
await user_api_key_auth(request=request, api_key=bearer_token)
|
|
# Handle DataError and other exceptions that don't have .message attribute
|
|
assert "Budget has been exceeded" in getattr(exc_info.value, "message", str(exc_info.value))
|
|
|
|
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
@pytest.mark.asyncio()
|
|
async def test_aview_spend_per_user(prisma_client):
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
try:
|
|
user_by_spend = await spend_user_fn(user_id=None)
|
|
assert type(user_by_spend) == list
|
|
assert len(user_by_spend) > 0
|
|
first_user = user_by_spend[0]
|
|
|
|
print("\nfirst_user=", first_user)
|
|
assert first_user["spend"] >= 0
|
|
except Exception as e:
|
|
print("Got Exception", e)
|
|
pytest.fail(f"Got exception {e}")
|
|
|
|
|
|
@pytest.mark.asyncio()
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
async def test_view_spend_per_key(prisma_client):
|
|
"""
|
|
Test viewing spend per key.
|
|
"""
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
try:
|
|
# First create a key to ensure there's data to query
|
|
request = GenerateKeyRequest(models=["gpt-3.5-turbo"], max_budget=100)
|
|
key = await generate_key_fn(
|
|
request,
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="test_user_spend",
|
|
),
|
|
)
|
|
print(f"Created test key: {key.key}")
|
|
|
|
# Now query spend
|
|
key_by_spend = await spend_key_fn()
|
|
assert type(key_by_spend) == list
|
|
|
|
# The list might be empty if no spend has been recorded yet - that's okay
|
|
if len(key_by_spend) > 0:
|
|
first_key = key_by_spend[0]
|
|
print("\nfirst_key=", first_key)
|
|
assert first_key.spend >= 0
|
|
else:
|
|
print("No keys with spend found (expected for new database)")
|
|
except Exception as e:
|
|
print(f"Got Exception: {e}")
|
|
# If it's a 400 error with empty message, it might be an empty database - that's okay
|
|
error_str = str(e)
|
|
if "400" in error_str and (
|
|
"error" in error_str.lower() or not error_str.strip()
|
|
):
|
|
print("Empty database or no spend data - test passes")
|
|
else:
|
|
pytest.fail(f"Got unexpected exception {e}")
|
|
|
|
|
|
@pytest.mark.asyncio()
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
async def test_key_name_null(prisma_client):
|
|
"""
|
|
- create key
|
|
- get key info
|
|
- assert key_name is null
|
|
"""
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
os.environ["DISABLE_KEY_NAME"] = "True"
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
try:
|
|
request = GenerateKeyRequest()
|
|
key = await generate_key_fn(
|
|
request,
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
print("generated key=", key)
|
|
generated_key = key.key
|
|
result = await info_key_fn(
|
|
key=generated_key,
|
|
user_api_key_dict=UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN),
|
|
)
|
|
print("result from info_key_fn", result)
|
|
assert result["info"]["key_name"] is None
|
|
except Exception as e:
|
|
print("Got Exception", e)
|
|
pytest.fail(f"Got exception {e}")
|
|
finally:
|
|
os.environ["DISABLE_KEY_NAME"] = "False"
|
|
|
|
|
|
@pytest.mark.asyncio()
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
async def test_key_name_set(prisma_client):
|
|
"""
|
|
- create key
|
|
- get key info
|
|
- assert key_name is not null
|
|
"""
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
setattr(litellm.proxy.proxy_server, "general_settings", {"allow_user_auth": True})
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
try:
|
|
request = GenerateKeyRequest()
|
|
key = await generate_key_fn(
|
|
request,
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
generated_key = key.key
|
|
result = await info_key_fn(
|
|
key=generated_key,
|
|
user_api_key_dict=UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN),
|
|
)
|
|
print("result from info_key_fn", result)
|
|
assert isinstance(result["info"]["key_name"], str)
|
|
except Exception as e:
|
|
print("Got Exception", e)
|
|
pytest.fail(f"Got exception {e}")
|
|
|
|
|
|
@pytest.mark.asyncio()
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
async def test_default_key_params(prisma_client):
|
|
"""
|
|
- create key
|
|
- get key info
|
|
- assert key_name is not null
|
|
"""
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
setattr(litellm.proxy.proxy_server, "general_settings", {"allow_user_auth": True})
|
|
litellm.default_key_generate_params = {"max_budget": 0.000122}
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
try:
|
|
request = GenerateKeyRequest()
|
|
key = await generate_key_fn(
|
|
request,
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
generated_key = key.key
|
|
result = await info_key_fn(
|
|
key=generated_key,
|
|
user_api_key_dict=UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN),
|
|
)
|
|
print("result from info_key_fn", result)
|
|
assert result["info"]["max_budget"] == 0.000122
|
|
except Exception as e:
|
|
print("Got Exception", e)
|
|
pytest.fail(f"Got exception {e}")
|
|
|
|
|
|
@pytest.mark.asyncio()
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
async def test_upperbound_key_param_larger_budget(prisma_client):
|
|
"""
|
|
- create key
|
|
- get key info
|
|
- assert key_name is not null
|
|
"""
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
litellm.upperbound_key_generate_params = LiteLLM_UpperboundKeyGenerateParams(
|
|
max_budget=0.001, budget_duration="1m"
|
|
)
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
request = GenerateKeyRequest(
|
|
max_budget=200000,
|
|
budget_duration="30d",
|
|
)
|
|
with pytest.raises(ProxyException) as exc_info:
|
|
await generate_key_fn(
|
|
request,
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
assert exc_info.value.code == str(400)
|
|
|
|
|
|
@pytest.mark.asyncio()
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
async def test_upperbound_key_param_larger_duration(prisma_client):
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
litellm.upperbound_key_generate_params = LiteLLM_UpperboundKeyGenerateParams(
|
|
max_budget=100, duration="14d"
|
|
)
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
request = GenerateKeyRequest(
|
|
max_budget=10,
|
|
duration="30d",
|
|
)
|
|
with pytest.raises(ProxyException) as exc_info:
|
|
await generate_key_fn(
|
|
request,
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
assert exc_info.value.code == str(400)
|
|
|
|
|
|
@pytest.mark.asyncio()
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
async def test_upperbound_key_param_none_duration(prisma_client):
|
|
from datetime import datetime, timedelta
|
|
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
litellm.upperbound_key_generate_params = LiteLLM_UpperboundKeyGenerateParams(
|
|
max_budget=100, duration="14d"
|
|
)
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
try:
|
|
request = GenerateKeyRequest()
|
|
key = await generate_key_fn(
|
|
request,
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
|
|
print(key)
|
|
# print(result)
|
|
|
|
assert key.max_budget == 100
|
|
assert key.expires is not None
|
|
|
|
_date_key_expires = key.expires.date()
|
|
_fourteen_days_from_now = (datetime.now() + timedelta(days=14)).date()
|
|
|
|
assert _date_key_expires == _fourteen_days_from_now
|
|
except Exception as e:
|
|
pytest.fail(f"Got exception {e}")
|
|
|
|
|
|
def test_get_bearer_token():
|
|
from litellm.proxy.auth.user_api_key_auth import _get_bearer_token
|
|
|
|
# Test valid Bearer token
|
|
api_key = "Bearer valid_token"
|
|
result = _get_bearer_token(api_key)
|
|
assert result == "valid_token", f"Expected 'valid_token', got '{result}'"
|
|
|
|
# Test empty API key
|
|
api_key = ""
|
|
result = _get_bearer_token(api_key)
|
|
assert result == "", f"Expected '', got '{result}'"
|
|
|
|
# Test API key without Bearer prefix
|
|
api_key = "invalid_token"
|
|
result = _get_bearer_token(api_key)
|
|
assert result == "", f"Expected '', got '{result}'"
|
|
|
|
# Test API key with Bearer prefix and extra spaces
|
|
api_key = " Bearer valid_token "
|
|
result = _get_bearer_token(api_key)
|
|
assert result == "", f"Expected '', got '{result}'"
|
|
|
|
# Test API key with Bearer prefix and no token
|
|
api_key = "Bearer sk-1234"
|
|
result = _get_bearer_token(api_key)
|
|
assert result == "sk-1234", f"Expected 'valid_token', got '{result}'"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
async def test_update_logs_with_spend_logs_url(prisma_client):
|
|
"""
|
|
Unit test for making sure spend logs list is still updated when url passed in
|
|
"""
|
|
from litellm.proxy.db.db_spend_update_writer import DBSpendUpdateWriter
|
|
|
|
db_spend_update_writer = DBSpendUpdateWriter()
|
|
|
|
payload = {"startTime": datetime.now(), "endTime": datetime.now()}
|
|
await db_spend_update_writer._insert_spend_log_to_db(
|
|
payload=payload, prisma_client=prisma_client
|
|
)
|
|
|
|
assert len(prisma_client.spend_log_transactions) > 0
|
|
|
|
prisma_client.spend_log_transactions = []
|
|
|
|
spend_logs_url = ""
|
|
payload = {"startTime": datetime.now(), "endTime": datetime.now()}
|
|
await db_spend_update_writer._insert_spend_log_to_db(
|
|
payload=payload, spend_logs_url=spend_logs_url, prisma_client=prisma_client
|
|
)
|
|
|
|
assert len(prisma_client.spend_log_transactions) > 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
async def test_user_api_key_auth(prisma_client):
|
|
from litellm.proxy.proxy_server import ProxyException
|
|
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
setattr(litellm.proxy.proxy_server, "general_settings", {"allow_user_auth": True})
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
|
|
request = Request(scope={"type": "http"})
|
|
request._url = URL(url="/chat/completions")
|
|
# Test case: No API Key passed in
|
|
with pytest.raises(ProxyException) as exc_info:
|
|
await user_api_key_auth(request, api_key=None)
|
|
exc = exc_info.value
|
|
print(exc.message)
|
|
assert exc.message == "Authentication Error, No api key passed in."
|
|
|
|
# Test case: Malformed API Key (missing 'Bearer ' prefix)
|
|
with pytest.raises(ProxyException) as exc_info:
|
|
await user_api_key_auth(request, api_key="my_token")
|
|
exc = exc_info.value
|
|
print(exc.message)
|
|
assert (
|
|
exc.message
|
|
== "Authentication Error, Malformed API Key passed in. Ensure Key has `Bearer ` prefix."
|
|
)
|
|
|
|
# Test case: User passes empty string API Key
|
|
with pytest.raises(ProxyException) as exc_info:
|
|
await user_api_key_auth(request, api_key="")
|
|
exc = exc_info.value
|
|
print(exc.message)
|
|
assert (
|
|
"Authentication Error, Malformed API Key passed in. Ensure Key has `Bearer ` prefix."
|
|
in exc.message
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
async def test_user_api_key_auth_without_master_key(prisma_client):
|
|
# if master key is not set, expect all calls to go through
|
|
try:
|
|
from litellm.proxy.proxy_server import ProxyException
|
|
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", None)
|
|
setattr(
|
|
litellm.proxy.proxy_server, "general_settings", {"allow_user_auth": True}
|
|
)
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
|
|
request = Request(scope={"type": "http"})
|
|
request._url = URL(url="/chat/completions")
|
|
# Test case: No API Key passed in
|
|
|
|
await user_api_key_auth(request, api_key=None)
|
|
await user_api_key_auth(request, api_key="my_token")
|
|
await user_api_key_auth(request, api_key="")
|
|
await user_api_key_auth(request, api_key="Bearer " + "1234")
|
|
except Exception as e:
|
|
print("Got Exception", e)
|
|
pytest.fail(f"Got exception {e}")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
async def test_key_with_no_permissions(prisma_client):
|
|
"""
|
|
- create key
|
|
- get key info
|
|
- assert key_name is null
|
|
"""
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
setattr(litellm.proxy.proxy_server, "general_settings", {"allow_user_auth": False})
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
try:
|
|
response = await generate_key_helper_fn(
|
|
request_type="key",
|
|
**{"duration": "1hr", "key_max_budget": 0, "models": [], "aliases": {}, "config": {}, "spend": 0, "user_id": "ishaan", "team_id": "litellm-dashboard"}, # type: ignore
|
|
)
|
|
|
|
print(response)
|
|
key = response["token"]
|
|
|
|
# make a /chat/completions call -> it should fail
|
|
request = Request(scope={"type": "http"})
|
|
request._url = URL(url="/chat/completions")
|
|
|
|
# use generated key to auth in
|
|
result = await user_api_key_auth(request=request, api_key="Bearer " + key)
|
|
print("result from user auth with new key", result)
|
|
pytest.fail(f"This should have failed!. IT's an invalid key")
|
|
except Exception as e:
|
|
print("Got Exception", e)
|
|
print(e.message)
|
|
|
|
|
|
async def track_cost_callback_helper_fn(generated_key: str, user_id: str):
|
|
from litellm._uuid import uuid
|
|
|
|
from litellm import Choices, Message, ModelResponse, Usage
|
|
from litellm.proxy.proxy_server import _ProxyDBLogger
|
|
|
|
request_id = f"chatcmpl-e41836bb-bb8b-4df2-8e70-8f3e160155ac{uuid.uuid4()}"
|
|
resp = ModelResponse(
|
|
id=request_id,
|
|
choices=[
|
|
Choices(
|
|
finish_reason=None,
|
|
index=0,
|
|
message=Message(
|
|
content=" Sure! Here is a short poem about the sky:\n\nA canvas of blue, a",
|
|
role="assistant",
|
|
),
|
|
)
|
|
],
|
|
model="gpt-35-turbo", # azure always has model written like this
|
|
usage=Usage(prompt_tokens=210, completion_tokens=200, total_tokens=410),
|
|
)
|
|
proxy_db_logger = _ProxyDBLogger()
|
|
await proxy_db_logger._PROXY_track_cost_callback(
|
|
kwargs={
|
|
"call_type": "acompletion",
|
|
"model": "sagemaker-chatgpt-v-3",
|
|
"stream": True,
|
|
"complete_streaming_response": resp,
|
|
"litellm_params": {
|
|
"metadata": {
|
|
"user_api_key": hash_token(generated_key),
|
|
"user_api_key_user_id": user_id,
|
|
}
|
|
},
|
|
"response_cost": 0.00005,
|
|
},
|
|
completion_response=resp,
|
|
start_time=datetime.now(),
|
|
end_time=datetime.now(),
|
|
)
|
|
|
|
|
|
@pytest.mark.skip(reason="High traffic load test for spend tracking")
|
|
@pytest.mark.asyncio
|
|
async def test_proxy_load_test_db(prisma_client):
|
|
"""
|
|
Run 1500 req./s against track_cost_callback function
|
|
"""
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
import logging
|
|
import time
|
|
|
|
from litellm._logging import verbose_proxy_logger
|
|
|
|
litellm.set_verbose = True
|
|
verbose_proxy_logger.setLevel(logging.DEBUG)
|
|
try:
|
|
start_time = time.time()
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
request = GenerateKeyRequest(max_budget=0.00001)
|
|
key = await generate_key_fn(
|
|
request,
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
print(key)
|
|
|
|
generated_key = key.key
|
|
user_id = key.user_id
|
|
bearer_token = "Bearer " + generated_key
|
|
|
|
request = Request(scope={"type": "http"})
|
|
request._url = URL(url="/chat/completions")
|
|
|
|
# use generated key to auth in
|
|
result = await user_api_key_auth(request=request, api_key=bearer_token)
|
|
print("result from user auth with new key", result)
|
|
# update spend using track_cost callback, make 2nd request, it should fail
|
|
n = 5000
|
|
tasks = [
|
|
track_cost_callback_helper_fn(generated_key=generated_key, user_id=user_id)
|
|
for _ in range(n)
|
|
]
|
|
completions = await asyncio.gather(*tasks)
|
|
await asyncio.sleep(120)
|
|
try:
|
|
# call spend logs
|
|
spend_logs = await view_spend_logs(
|
|
api_key=generated_key,
|
|
user_api_key_dict=UserAPIKeyAuth(api_key=generated_key),
|
|
)
|
|
|
|
print(f"len responses: {len(spend_logs)}")
|
|
assert len(spend_logs) == n
|
|
print(n, time.time() - start_time, len(spend_logs))
|
|
except Exception:
|
|
print(n, time.time() - start_time, 0)
|
|
raise Exception(f"it worked! key={key.key}")
|
|
except Exception as e:
|
|
pytest.fail(f"An exception occurred - {str(e)}")
|
|
|
|
|
|
@pytest.mark.asyncio()
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
async def test_master_key_hashing(prisma_client):
|
|
try:
|
|
from litellm._uuid import uuid
|
|
|
|
print("prisma client=", prisma_client)
|
|
|
|
master_key = "sk-1234"
|
|
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", master_key)
|
|
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
from litellm.proxy.proxy_server import user_api_key_cache
|
|
|
|
_team_id = "ishaans-special-team_{}".format(uuid.uuid4())
|
|
user_api_key_dict = UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
)
|
|
await new_team(
|
|
NewTeamRequest(team_id=_team_id),
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
http_request=Request(scope={"type": "http"}),
|
|
)
|
|
|
|
_response = await new_user(
|
|
data=NewUserRequest(
|
|
models=["azure-gpt-3.5"],
|
|
team_id=_team_id,
|
|
tpm_limit=20,
|
|
),
|
|
user_api_key_dict=user_api_key_dict,
|
|
)
|
|
print(_response)
|
|
assert _response.models == ["azure-gpt-3.5"]
|
|
assert _response.team_id == _team_id
|
|
assert _response.tpm_limit == 20
|
|
|
|
bearer_token = "Bearer " + master_key
|
|
|
|
request = Request(scope={"type": "http"})
|
|
request._url = URL(url="/chat/completions")
|
|
|
|
# use generated key to auth in
|
|
result: UserAPIKeyAuth = await user_api_key_auth(
|
|
request=request, api_key=bearer_token
|
|
)
|
|
|
|
# Master-key auth substitutes a stable alias so the master key (or
|
|
# its hash) never propagates into spend logs / metrics / audit trails.
|
|
from litellm.constants import LITELLM_PROXY_MASTER_KEY_ALIAS
|
|
|
|
assert result.api_key == LITELLM_PROXY_MASTER_KEY_ALIAS
|
|
assert result.api_key != hash_token(master_key)
|
|
|
|
except Exception as e:
|
|
print("Got Exception", e)
|
|
pytest.fail(f"Got exception {e}")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
async def test_reset_spend_authentication(prisma_client):
|
|
"""
|
|
1. Test master key can access this route -> ONLY MASTER KEY SHOULD BE ABLE TO RESET SPEND
|
|
2. Test that non-master key gets rejected
|
|
3. Test that non-master key with role == LitellmUserRoles.PROXY_ADMIN or admin gets rejected
|
|
"""
|
|
|
|
print("prisma client=", prisma_client)
|
|
|
|
master_key = "sk-1234"
|
|
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", master_key)
|
|
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
from litellm.proxy.proxy_server import user_api_key_cache
|
|
|
|
bearer_token = "Bearer " + master_key
|
|
|
|
request = Request(scope={"type": "http"})
|
|
request._url = URL(url="/global/spend/reset")
|
|
|
|
# Test 1 - Master Key
|
|
result: UserAPIKeyAuth = await user_api_key_auth(
|
|
request=request, api_key=bearer_token
|
|
)
|
|
|
|
print("result from user auth with Master key", result)
|
|
assert result.token is not None
|
|
|
|
# Test 2 - Non-Master Key
|
|
_response = await new_user(
|
|
data=NewUserRequest(
|
|
tpm_limit=20,
|
|
),
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key=master_key,
|
|
user_id="1234",
|
|
),
|
|
)
|
|
|
|
generate_key = "Bearer " + _response.key
|
|
|
|
with pytest.raises(
|
|
Exception, match="Tried to access route=/global/spend/reset, which is only for MASTER KEY"
|
|
) as exc_info:
|
|
await user_api_key_auth(request=request, api_key=generate_key)
|
|
e = exc_info.value
|
|
print("Got Exception", e)
|
|
assert (
|
|
"Tried to access route=/global/spend/reset, which is only for MASTER KEY"
|
|
in e.message
|
|
)
|
|
|
|
# Test 3 - Non-Master Key with role == LitellmUserRoles.PROXY_ADMIN or admin
|
|
_response = await new_user(
|
|
data=NewUserRequest(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
tpm_limit=20,
|
|
),
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key=master_key,
|
|
user_id="1234",
|
|
),
|
|
)
|
|
|
|
generate_key = "Bearer " + _response.key
|
|
|
|
with pytest.raises(
|
|
Exception, match="Tried to access route=/global/spend/reset, which is only for MASTER KEY"
|
|
) as exc_info:
|
|
await user_api_key_auth(request=request, api_key=generate_key)
|
|
e = exc_info.value
|
|
print("Got Exception", e)
|
|
assert (
|
|
"Tried to access route=/global/spend/reset, which is only for MASTER KEY"
|
|
in e.message
|
|
)
|
|
|
|
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
@pytest.mark.asyncio()
|
|
async def test_create_update_team(prisma_client):
|
|
"""
|
|
- Set max_budget, budget_duration, max_budget, tpm_limit, rpm_limit
|
|
- Assert response has correct values
|
|
|
|
- Update max_budget, budget_duration, max_budget, tpm_limit, rpm_limit
|
|
- Assert response has correct values
|
|
|
|
- Call team_info and assert response has correct values
|
|
"""
|
|
print("prisma client=", prisma_client)
|
|
|
|
master_key = "sk-1234"
|
|
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", master_key)
|
|
import datetime
|
|
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
from litellm.proxy.proxy_server import user_api_key_cache
|
|
|
|
_team_id = "test-team_{}".format(uuid.uuid4())
|
|
response = await new_team(
|
|
NewTeamRequest(
|
|
team_id=_team_id,
|
|
max_budget=20,
|
|
budget_duration="30d",
|
|
tpm_limit=20,
|
|
rpm_limit=20,
|
|
),
|
|
http_request=Request(scope={"type": "http"}),
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
|
|
print("RESPONSE from new_team", response)
|
|
|
|
assert response["team_id"] == _team_id
|
|
assert response["max_budget"] == 20
|
|
assert response["tpm_limit"] == 20
|
|
assert response["rpm_limit"] == 20
|
|
assert response["budget_duration"] == "30d"
|
|
assert response["budget_reset_at"] is not None and isinstance(
|
|
response["budget_reset_at"], datetime.datetime
|
|
)
|
|
|
|
# updating team budget duration and reset at
|
|
|
|
response = await update_team(
|
|
UpdateTeamRequest(
|
|
team_id=_team_id,
|
|
max_budget=30,
|
|
budget_duration="2d",
|
|
tpm_limit=30,
|
|
rpm_limit=30,
|
|
),
|
|
http_request=Request(scope={"type": "http"}),
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
|
|
print("RESPONSE from update_team", response)
|
|
_updated_info = response["data"]
|
|
_updated_info = dict(_updated_info)
|
|
|
|
assert _updated_info["team_id"] == _team_id
|
|
assert _updated_info["max_budget"] == 30
|
|
assert _updated_info["tpm_limit"] == 30
|
|
assert _updated_info["rpm_limit"] == 30
|
|
assert _updated_info["budget_duration"] == "2d"
|
|
assert _updated_info["budget_reset_at"] is not None and isinstance(
|
|
_updated_info["budget_reset_at"], datetime.datetime
|
|
)
|
|
|
|
# budget_reset_at should be 2 days from now
|
|
budget_reset_at = _updated_info["budget_reset_at"].replace(tzinfo=timezone.utc)
|
|
current_time = datetime.datetime.now(timezone.utc)
|
|
|
|
# Verify that budget_reset_at is at midnight (hour, minute, second are all 0)
|
|
assert budget_reset_at.hour == 0
|
|
assert budget_reset_at.minute == 0
|
|
assert budget_reset_at.second == 0
|
|
|
|
# Calculate days difference - should be close to 2 days (within 1 day to account for time of test execution)
|
|
days_diff = (budget_reset_at.date() - current_time.date()).days
|
|
assert 1 <= days_diff <= 2
|
|
|
|
# now hit team_info
|
|
try:
|
|
response = await team_info(
|
|
team_id=_team_id,
|
|
http_request=Request(scope={"type": "http"}),
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
except Exception as e:
|
|
print(e)
|
|
pytest.fail("Receives error - {}".format(e))
|
|
|
|
_team_info = response["team_info"]
|
|
_team_info = dict(_team_info)
|
|
|
|
assert _team_info["team_id"] == _team_id
|
|
assert _team_info["max_budget"] == 30
|
|
assert _team_info["tpm_limit"] == 30
|
|
assert _team_info["rpm_limit"] == 30
|
|
assert _team_info["budget_duration"] == "2d"
|
|
assert _team_info["budget_reset_at"] is not None and isinstance(
|
|
_team_info["budget_reset_at"], datetime.datetime
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio()
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
async def test_update_user_role(prisma_client):
|
|
"""
|
|
Tests if we update user role, incorrect values are not stored in cache
|
|
-> create a user with role == INTERNAL_USER
|
|
-> access an Admin only route -> expect to fail
|
|
-> update user role to == PROXY_ADMIN
|
|
-> access an Admin only route -> expect to succeed
|
|
"""
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
key = await new_user(
|
|
data=NewUserRequest(
|
|
user_role=LitellmUserRoles.INTERNAL_USER,
|
|
)
|
|
)
|
|
|
|
print(key)
|
|
api_key = "Bearer " + key.key
|
|
|
|
api_route = APIRoute(path="/global/spend", endpoint=global_spend)
|
|
request = Request(
|
|
{
|
|
"type": "http",
|
|
"route": api_route,
|
|
"path": "/global/spend",
|
|
"headers": [("Authorization", api_key)],
|
|
}
|
|
)
|
|
|
|
request._url = URL(url="/global/spend")
|
|
|
|
# use generated key to auth in
|
|
try:
|
|
result = await user_api_key_auth(request=request, api_key=api_key)
|
|
print("result from user auth with new key", result)
|
|
except Exception as e:
|
|
print(e)
|
|
pass
|
|
|
|
await user_update(
|
|
data=UpdateUserRequest(
|
|
user_id=key.user_id, user_role=LitellmUserRoles.PROXY_ADMIN
|
|
),
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
|
|
# await asyncio.sleep(3)
|
|
|
|
# use generated key to auth in
|
|
print("\n\nMAKING NEW REQUEST WITH UPDATED USER ROLE\n\n")
|
|
result = await user_api_key_auth(request=request, api_key=api_key)
|
|
print("result from user auth with new key", result)
|
|
|
|
|
|
@pytest.mark.asyncio()
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
async def test_update_user_unit_test(prisma_client):
|
|
"""
|
|
Unit test for /user/update
|
|
|
|
Ensure that params are updated for UpdateUserRequest
|
|
"""
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
key = await new_user(
|
|
data=NewUserRequest(
|
|
user_email=f"test-{uuid.uuid4()}@test.com",
|
|
)
|
|
)
|
|
|
|
print(key)
|
|
|
|
user_info = await user_update(
|
|
data=UpdateUserRequest(
|
|
user_id=key.user_id,
|
|
team_id="1234",
|
|
max_budget=100,
|
|
budget_duration="10d",
|
|
tpm_limit=100,
|
|
rpm_limit=100,
|
|
metadata={"very-new-metadata": "something"},
|
|
),
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
|
|
print("user_info", user_info)
|
|
assert user_info is not None
|
|
_user_info = user_info["data"].model_dump()
|
|
|
|
assert _user_info["user_id"] == key.user_id
|
|
assert _user_info["team_id"] == "1234"
|
|
assert _user_info["max_budget"] == 100
|
|
assert _user_info["budget_duration"] == "10d"
|
|
assert _user_info["tpm_limit"] == 100
|
|
assert _user_info["rpm_limit"] == 100
|
|
assert _user_info["metadata"] == {"very-new-metadata": "something"}
|
|
|
|
# budget_reset_at should be at midnight 10 days from now
|
|
budget_reset_at = _user_info["budget_reset_at"].replace(tzinfo=timezone.utc)
|
|
current_time = datetime.now(timezone.utc)
|
|
|
|
# Verify that budget_reset_at is at midnight (hour, minute, second are all 0)
|
|
assert budget_reset_at.hour == 0
|
|
assert budget_reset_at.minute == 0
|
|
assert budget_reset_at.second == 0
|
|
|
|
# Calculate days difference - should be close to 10 days (within 1 day to account for time of test execution)
|
|
days_diff = (budget_reset_at.date() - current_time.date()).days
|
|
assert 9 <= days_diff <= 10
|
|
|
|
|
|
@pytest.mark.asyncio()
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
async def test_custom_api_key_header_name(prisma_client):
|
|
""" """
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
setattr(
|
|
litellm.proxy.proxy_server,
|
|
"general_settings",
|
|
{"litellm_key_header_name": "x-litellm-key"},
|
|
)
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
|
|
api_route = APIRoute(path="/chat/completions", endpoint=chat_completion)
|
|
request = Request(
|
|
{
|
|
"type": "http",
|
|
"route": api_route,
|
|
"path": api_route.path,
|
|
"headers": [
|
|
(b"x-litellm-key", b"Bearer sk-1234"),
|
|
],
|
|
}
|
|
)
|
|
|
|
# this should pass because we pass the master key as X-Litellm-Key and litellm_key_header_name="X-Litellm-Key" in general settings
|
|
result = await user_api_key_auth(request=request, api_key="Bearer invalid-key")
|
|
|
|
# this should fail because X-Litellm-Key is invalid
|
|
request = Request(
|
|
{
|
|
"type": "http",
|
|
"route": api_route,
|
|
"path": api_route.path,
|
|
"headers": [],
|
|
}
|
|
)
|
|
with pytest.raises(
|
|
Exception, match=re.escape("Malformed API Key passed in. Ensure Key has `Bearer ` prefix")
|
|
) as exc_info:
|
|
result = await user_api_key_auth(request=request, api_key="Bearer sk-1234")
|
|
e = exc_info.value
|
|
print("failed with error", e)
|
|
assert (
|
|
"Malformed API Key passed in. Ensure Key has `Bearer ` prefix" in e.message
|
|
)
|
|
|
|
# this should pass because X-Litellm-Key is valid
|
|
|
|
|
|
@pytest.mark.asyncio()
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
async def test_generate_key_with_model_tpm_limit(prisma_client):
|
|
print("prisma client=", prisma_client)
|
|
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
request = GenerateKeyRequest(
|
|
metadata={
|
|
"team": "litellm-team3",
|
|
"model_tpm_limit": {"gpt-4": 100},
|
|
"model_rpm_limit": {"gpt-4": 2},
|
|
}
|
|
)
|
|
key = await generate_key_fn(
|
|
data=request,
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
print(key)
|
|
|
|
generated_key = key.key
|
|
|
|
# use generated key to auth in
|
|
result = await info_key_fn(
|
|
key=generated_key,
|
|
user_api_key_dict=UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN),
|
|
)
|
|
print("result from info_key_fn", result)
|
|
assert result["key"] == generated_key
|
|
print("\n info for key=", result["info"])
|
|
assert result["info"]["metadata"] == {
|
|
"team": "litellm-team3",
|
|
"model_tpm_limit": {"gpt-4": 100},
|
|
"model_rpm_limit": {"gpt-4": 2},
|
|
}
|
|
|
|
# Update model tpm_limit and rpm_limit
|
|
request = UpdateKeyRequest(
|
|
key=generated_key,
|
|
model_tpm_limit={"gpt-4": 200},
|
|
model_rpm_limit={"gpt-4": 3},
|
|
)
|
|
_request = Request(scope={"type": "http"})
|
|
_request._url = URL(url="/update/key")
|
|
|
|
await update_key_fn(
|
|
data=request,
|
|
request=_request,
|
|
user_api_key_dict=UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN),
|
|
)
|
|
result = await info_key_fn(
|
|
key=generated_key,
|
|
user_api_key_dict=UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN),
|
|
)
|
|
print("result from info_key_fn", result)
|
|
assert result["key"] == generated_key
|
|
print("\n info for key=", result["info"])
|
|
assert result["info"]["metadata"] == {
|
|
"team": "litellm-team3",
|
|
"model_tpm_limit": {"gpt-4": 200},
|
|
"model_rpm_limit": {"gpt-4": 3},
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio()
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
async def test_generate_key_with_guardrails(prisma_client):
|
|
print("prisma client=", prisma_client)
|
|
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
request = GenerateKeyRequest(
|
|
guardrails=["aporia-pre-call"],
|
|
metadata={
|
|
"team": "litellm-team3",
|
|
},
|
|
)
|
|
key = await generate_key_fn(
|
|
data=request,
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
print("generated key=", key)
|
|
|
|
generated_key = key.key
|
|
|
|
# use generated key to auth in
|
|
result = await info_key_fn(
|
|
key=generated_key,
|
|
user_api_key_dict=UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN),
|
|
)
|
|
print("result from info_key_fn", result)
|
|
assert result["key"] == generated_key
|
|
print("\n info for key=", result["info"])
|
|
assert result["info"]["metadata"] == {
|
|
"team": "litellm-team3",
|
|
"guardrails": ["aporia-pre-call"],
|
|
}
|
|
|
|
# Update model tpm_limit and rpm_limit
|
|
request = UpdateKeyRequest(
|
|
key=generated_key,
|
|
guardrails=["aporia-pre-call", "aporia-post-call"],
|
|
)
|
|
_request = Request(scope={"type": "http"})
|
|
_request._url = URL(url="/update/key")
|
|
|
|
await update_key_fn(
|
|
data=request,
|
|
request=_request,
|
|
user_api_key_dict=UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN),
|
|
)
|
|
result = await info_key_fn(
|
|
key=generated_key,
|
|
user_api_key_dict=UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN),
|
|
)
|
|
print("result from info_key_fn", result)
|
|
assert result["key"] == generated_key
|
|
print("\n info for key=", result["info"])
|
|
assert result["info"]["metadata"] == {
|
|
"team": "litellm-team3",
|
|
"guardrails": ["aporia-pre-call", "aporia-post-call"],
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio()
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
async def test_team_guardrails(prisma_client):
|
|
"""
|
|
- Test setting guardrails on a team
|
|
- Assert this is returned when calling /team/info
|
|
- Team/update with guardrails should update the guardrails
|
|
- Assert new guardrails are returned when calling /team/info
|
|
"""
|
|
litellm.set_verbose = True
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
|
|
_new_team = NewTeamRequest(
|
|
team_alias="test-teamA",
|
|
guardrails=["aporia-pre-call"],
|
|
)
|
|
|
|
new_team_response = await new_team(
|
|
data=_new_team,
|
|
user_api_key_dict=UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN),
|
|
http_request=Request(scope={"type": "http"}),
|
|
)
|
|
|
|
print("new_team_response", new_team_response)
|
|
|
|
# call /team/info
|
|
team_info_response = await team_info(
|
|
team_id=new_team_response["team_id"],
|
|
user_api_key_dict=UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN),
|
|
http_request=Request(scope={"type": "http"}),
|
|
)
|
|
print("team_info_response", team_info_response)
|
|
|
|
assert team_info_response["team_info"].metadata["guardrails"] == ["aporia-pre-call"]
|
|
|
|
# team update with guardrails
|
|
team_update_response = await update_team(
|
|
data=UpdateTeamRequest(
|
|
team_id=new_team_response["team_id"],
|
|
guardrails=["aporia-pre-call", "aporia-post-call"],
|
|
),
|
|
user_api_key_dict=UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN),
|
|
http_request=Request(scope={"type": "http"}),
|
|
)
|
|
|
|
print("team_update_response", team_update_response)
|
|
|
|
# call /team/info again
|
|
team_info_response = await team_info(
|
|
team_id=new_team_response["team_id"],
|
|
user_api_key_dict=UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN),
|
|
http_request=Request(scope={"type": "http"}),
|
|
)
|
|
|
|
print("team_info_response", team_info_response)
|
|
assert team_info_response["team_info"].metadata["guardrails"] == [
|
|
"aporia-pre-call",
|
|
"aporia-post-call",
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio()
|
|
@pytest.mark.flaky(retries=6, delay=1)
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
async def test_team_access_groups(prisma_client):
|
|
"""
|
|
Test team based model access groups
|
|
|
|
- Test calling a model in the access group -> pass
|
|
- Test calling a model not in the access group -> fail
|
|
"""
|
|
litellm.set_verbose = True
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
# create router with access groups
|
|
litellm_router = litellm.Router(
|
|
model_list=[
|
|
{
|
|
"model_name": "gemini-pro-vision",
|
|
"litellm_params": {
|
|
"model": "vertex_ai/gemini-1.0-pro-vision-001",
|
|
},
|
|
"model_info": {"access_groups": ["beta-models"]},
|
|
},
|
|
{
|
|
"model_name": "gpt-4o",
|
|
"litellm_params": {
|
|
"model": "gpt-4o",
|
|
},
|
|
"model_info": {"access_groups": ["beta-models"]},
|
|
},
|
|
]
|
|
)
|
|
setattr(litellm.proxy.proxy_server, "llm_router", litellm_router)
|
|
|
|
# Create team with models=["beta-models"]
|
|
team_request = NewTeamRequest(
|
|
team_alias="testing-team",
|
|
models=["beta-models"],
|
|
)
|
|
|
|
new_team_response = await new_team(
|
|
data=team_request,
|
|
user_api_key_dict=UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN),
|
|
http_request=Request(scope={"type": "http"}),
|
|
)
|
|
print("new_team_response", new_team_response)
|
|
created_team_id = new_team_response["team_id"]
|
|
|
|
# create key with team_id=created_team_id
|
|
request = GenerateKeyRequest(
|
|
team_id=created_team_id,
|
|
)
|
|
|
|
key = await generate_key_fn(
|
|
data=request,
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
print(key)
|
|
|
|
generated_key = key.key
|
|
bearer_token = "Bearer " + generated_key
|
|
|
|
request._url = URL(url="/chat/completions")
|
|
|
|
for model in ["gpt-4o", "gemini-pro-vision"]:
|
|
# Expect these to pass
|
|
async def return_body():
|
|
return_string = f'{{"model": "{model}"}}'
|
|
# return string as bytes
|
|
return return_string.encode()
|
|
|
|
request = Request(scope={"type": "http"})
|
|
request._url = URL(url="/chat/completions")
|
|
request.body = return_body
|
|
|
|
# use generated key to auth in
|
|
print(
|
|
"Bearer token being sent to user_api_key_auth() - {}".format(bearer_token)
|
|
)
|
|
result = await user_api_key_auth(request=request, api_key=bearer_token)
|
|
|
|
for model in ["gpt-4", "gpt-4o-mini", "gemini-experimental"]:
|
|
# Expect these to fail
|
|
async def return_body_2():
|
|
return_string = f'{{"model": "{model}"}}'
|
|
# return string as bytes
|
|
return return_string.encode()
|
|
|
|
request = Request(scope={"type": "http"})
|
|
request._url = URL(url="/chat/completions")
|
|
request.body = return_body_2
|
|
|
|
# use generated key to auth in
|
|
print(
|
|
"Bearer token being sent to user_api_key_auth() - {}".format(bearer_token)
|
|
)
|
|
with pytest.raises(ProxyException) as exc_info:
|
|
result = await user_api_key_auth(request=request, api_key=bearer_token)
|
|
e = exc_info.value
|
|
print("got exception", e)
|
|
assert isinstance(e, ProxyException)
|
|
assert e.type == ProxyErrorTypes.team_model_access_denied
|
|
assert e.param == "model"
|
|
|
|
|
|
@pytest.mark.asyncio()
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
async def test_team_tags(prisma_client):
|
|
"""
|
|
- Test setting tags on a team
|
|
- Assert this is returned when calling /team/info
|
|
- Team/update with tags should update the tags
|
|
- Assert new tags are returned when calling /team/info
|
|
"""
|
|
litellm.set_verbose = True
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
|
|
_new_team = NewTeamRequest(
|
|
team_alias="test-teamA",
|
|
tags=["teamA"],
|
|
)
|
|
|
|
new_team_response = await new_team(
|
|
data=_new_team,
|
|
user_api_key_dict=UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN),
|
|
http_request=Request(scope={"type": "http"}),
|
|
)
|
|
|
|
print("new_team_response", new_team_response)
|
|
|
|
# call /team/info
|
|
team_info_response = await team_info(
|
|
team_id=new_team_response["team_id"],
|
|
user_api_key_dict=UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN),
|
|
http_request=Request(scope={"type": "http"}),
|
|
)
|
|
print("team_info_response", team_info_response)
|
|
|
|
assert team_info_response["team_info"].metadata["tags"] == ["teamA"]
|
|
|
|
# team update with tags
|
|
team_update_response = await update_team(
|
|
data=UpdateTeamRequest(
|
|
team_id=new_team_response["team_id"],
|
|
tags=["teamA", "teamB"],
|
|
),
|
|
user_api_key_dict=UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN),
|
|
http_request=Request(scope={"type": "http"}),
|
|
)
|
|
|
|
print("team_update_response", team_update_response)
|
|
|
|
# call /team/info again
|
|
team_info_response = await team_info(
|
|
team_id=new_team_response["team_id"],
|
|
user_api_key_dict=UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN),
|
|
http_request=Request(scope={"type": "http"}),
|
|
)
|
|
|
|
print("team_info_response", team_info_response)
|
|
assert team_info_response["team_info"].metadata["tags"] == ["teamA", "teamB"]
|
|
|
|
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
@pytest.mark.asyncio
|
|
async def test_aadmin_only_routes(prisma_client):
|
|
"""
|
|
Tests if setting admin_only_routes works
|
|
|
|
only an admin should be able to access admin only routes
|
|
"""
|
|
litellm.set_verbose = True
|
|
print(f"os.getenv('DATABASE_URL')={os.getenv('DATABASE_URL')}")
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
general_settings = {
|
|
"allowed_routes": ["/embeddings", "/key/generate"],
|
|
"admin_only_routes": ["/key/generate"],
|
|
}
|
|
from litellm.proxy import proxy_server
|
|
|
|
initial_general_settings = getattr(proxy_server, "general_settings")
|
|
|
|
setattr(proxy_server, "general_settings", general_settings)
|
|
|
|
admin_user = await new_user(
|
|
data=NewUserRequest(
|
|
user_name="admin",
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
),
|
|
user_api_key_dict=UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN),
|
|
)
|
|
|
|
non_admin_user = await new_user(
|
|
data=NewUserRequest(
|
|
user_name="non-admin",
|
|
user_role=LitellmUserRoles.INTERNAL_USER,
|
|
),
|
|
user_api_key_dict=UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN),
|
|
)
|
|
|
|
admin_user_key = admin_user.key
|
|
non_admin_user_key = non_admin_user.key
|
|
|
|
assert admin_user_key is not None
|
|
assert non_admin_user_key is not None
|
|
|
|
# assert non-admin can not access admin routes
|
|
request = Request(scope={"type": "http"})
|
|
request._url = URL(url="/key/generate")
|
|
await user_api_key_auth(
|
|
request=request,
|
|
api_key="Bearer " + admin_user_key,
|
|
)
|
|
|
|
# this should pass
|
|
|
|
try:
|
|
await user_api_key_auth(
|
|
request=request,
|
|
api_key="Bearer " + non_admin_user_key,
|
|
)
|
|
pytest.fail("Expected this call to fail. User is over limit.")
|
|
except Exception as e:
|
|
print("error str=", str(e.message))
|
|
error_str = str(e.message)
|
|
assert "Route" in error_str and "admin only route" in error_str
|
|
pass
|
|
|
|
setattr(proxy_server, "general_settings", initial_general_settings)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
async def test_list_keys(prisma_client):
|
|
"""
|
|
Test the list_keys function:
|
|
- Test basic key
|
|
- Test pagination
|
|
- Test filtering by user_id, and key_alias
|
|
"""
|
|
from fastapi import Query
|
|
|
|
from litellm.proxy.proxy_server import hash_token
|
|
from litellm.proxy._types import LitellmUserRoles
|
|
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
|
|
# Test basic listing
|
|
request = Request(scope={"type": "http", "query_string": b""})
|
|
response = await list_keys(
|
|
request,
|
|
UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN.value,
|
|
),
|
|
page=1,
|
|
size=10,
|
|
user_id=None,
|
|
team_id=None,
|
|
organization_id=None,
|
|
key_hash=None,
|
|
key_alias=None,
|
|
return_full_object=False,
|
|
include_team_keys=False,
|
|
include_created_by_keys=False,
|
|
sort_by=None,
|
|
sort_order="desc",
|
|
expand=None,
|
|
status=None,
|
|
)
|
|
print("response=", response)
|
|
assert "keys" in response
|
|
assert len(response["keys"]) > 0
|
|
assert "total_count" in response
|
|
assert "current_page" in response
|
|
assert "total_pages" in response
|
|
|
|
# Test pagination
|
|
response = await list_keys(
|
|
request,
|
|
UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN.value),
|
|
page=1,
|
|
size=2,
|
|
user_id=None,
|
|
team_id=None,
|
|
organization_id=None,
|
|
key_hash=None,
|
|
key_alias=None,
|
|
return_full_object=False,
|
|
include_team_keys=False,
|
|
include_created_by_keys=False,
|
|
sort_by=None,
|
|
sort_order="desc",
|
|
expand=None,
|
|
status=None,
|
|
)
|
|
print("pagination response=", response)
|
|
assert len(response["keys"]) == 2
|
|
assert response["current_page"] == 1
|
|
|
|
# Test filtering by user_id
|
|
|
|
unique_id = str(uuid.uuid4())
|
|
team_id = f"key-list-team-{unique_id}"
|
|
key_alias = f"key-list-alias-{unique_id}"
|
|
user_id = f"key-list-user-{unique_id}"
|
|
response = await new_user(
|
|
data=NewUserRequest(
|
|
user_id=f"key-list-user-{unique_id}",
|
|
user_role=LitellmUserRoles.INTERNAL_USER,
|
|
key_alias=f"key-list-alias-{unique_id}",
|
|
),
|
|
user_api_key_dict=UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN),
|
|
)
|
|
|
|
_key = hash_token(response.key)
|
|
|
|
await asyncio.sleep(2)
|
|
|
|
# Test filtering by user_id
|
|
response = await list_keys(
|
|
request,
|
|
UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN.value),
|
|
page=1,
|
|
size=10,
|
|
user_id=user_id,
|
|
team_id=None,
|
|
organization_id=None,
|
|
key_hash=None,
|
|
key_alias=None,
|
|
return_full_object=False,
|
|
include_team_keys=False,
|
|
include_created_by_keys=False,
|
|
sort_by=None,
|
|
sort_order="desc",
|
|
expand=None,
|
|
status=None,
|
|
)
|
|
print("filtered user_id response=", response)
|
|
assert len(response["keys"]) == 1
|
|
assert _key in response["keys"]
|
|
|
|
# Test filtering by key_alias
|
|
response = await list_keys(
|
|
request,
|
|
UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN.value),
|
|
page=1,
|
|
size=10,
|
|
user_id=None,
|
|
team_id=None,
|
|
organization_id=None,
|
|
key_hash=None,
|
|
key_alias=key_alias,
|
|
return_full_object=False,
|
|
include_team_keys=False,
|
|
include_created_by_keys=False,
|
|
sort_by=None,
|
|
sort_order="desc",
|
|
expand=None,
|
|
status=None,
|
|
)
|
|
assert len(response["keys"]) == 1
|
|
assert _key in response["keys"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
async def test_key_aliases(prisma_client):
|
|
"""
|
|
Test the key_aliases function:
|
|
- Returns a paginated response
|
|
- Includes alias from a newly created key
|
|
- Aliases are sorted
|
|
- Pagination and search params work correctly
|
|
"""
|
|
import asyncio
|
|
import uuid
|
|
import litellm
|
|
from litellm.proxy._types import LitellmUserRoles
|
|
|
|
# Wire up test prisma client
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
|
|
# Basic call - check pagination response shape
|
|
response = await key_aliases(page=1, size=50)
|
|
assert "aliases" in response
|
|
assert isinstance(response["aliases"], list)
|
|
assert "total_count" in response
|
|
assert "current_page" in response
|
|
assert "total_pages" in response
|
|
assert "size" in response
|
|
assert response["current_page"] == 1
|
|
assert response["size"] == 50
|
|
|
|
# Create a new user (and key) with a unique alias
|
|
unique_id = str(uuid.uuid4())
|
|
test_alias = f"key-aliases-test-{unique_id}"
|
|
test_user_id = f"key-aliases-user-{unique_id}"
|
|
|
|
await new_user(
|
|
data=NewUserRequest(
|
|
user_id=test_user_id,
|
|
user_role=LitellmUserRoles.INTERNAL_USER,
|
|
key_alias=test_alias,
|
|
),
|
|
user_api_key_dict=UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN),
|
|
)
|
|
|
|
# Allow async DB writes to settle
|
|
await asyncio.sleep(2)
|
|
|
|
# Call again and validate alias is present
|
|
response_after = await key_aliases(page=1, size=50)
|
|
aliases = response_after["aliases"]
|
|
assert test_alias in aliases
|
|
assert aliases == sorted(aliases)
|
|
|
|
# Search by partial alias
|
|
partial = test_alias[:10]
|
|
search_response = await key_aliases(page=1, size=50, search=partial)
|
|
assert test_alias in search_response["aliases"]
|
|
|
|
# Search with no match
|
|
no_match_response = await key_aliases(page=1, size=50, search="__no_match_xyz__")
|
|
assert len(no_match_response["aliases"]) == 0
|
|
assert no_match_response["total_count"] == 0
|
|
|
|
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
@pytest.mark.asyncio
|
|
async def test_auth_vertex_ai_route(prisma_client):
|
|
"""
|
|
If user is premium user and vertex-ai route is used. Assert Virtual Key checks are run
|
|
"""
|
|
litellm.set_verbose = True
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "premium_user", True)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
|
|
route = "/vertex-ai/publishers/google/models/gemini-1.5-flash-001:generateContent"
|
|
request = Request(scope={"type": "http"})
|
|
request._url = URL(url=route)
|
|
request._headers = {"Authorization": "Bearer sk-12345"}
|
|
with pytest.raises(Exception, match="Invalid proxy server token passed") as exc_info:
|
|
await user_api_key_auth(request=request, api_key="Bearer " + "sk-12345")
|
|
e = exc_info.value
|
|
print(vars(e))
|
|
print("error str=", str(e.message))
|
|
error_str = str(e.message)
|
|
assert e.code == "401"
|
|
assert "Invalid proxy server token passed" in error_str
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_user_api_key_auth_db_unavailable():
|
|
"""
|
|
Test that user_api_key_auth handles DB connection failures appropriately when:
|
|
1. DB connection fails during token validation
|
|
2. allow_requests_on_db_unavailable=True
|
|
"""
|
|
litellm.set_verbose = True
|
|
|
|
# Mock dependencies
|
|
class MockPrismaClient:
|
|
async def get_data(self, *args, **kwargs):
|
|
print("MockPrismaClient.get_data() called")
|
|
raise httpx.ConnectError("Failed to connect to DB")
|
|
|
|
async def connect(self):
|
|
print("MockPrismaClient.connect() called")
|
|
pass
|
|
|
|
class MockDualCache:
|
|
async def async_get_cache(self, *args, **kwargs):
|
|
return None
|
|
|
|
async def async_set_cache(self, *args, **kwargs):
|
|
pass
|
|
|
|
async def set_cache(self, *args, **kwargs):
|
|
pass
|
|
|
|
# Set up test environment
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", MockPrismaClient())
|
|
setattr(litellm.proxy.proxy_server, "user_api_key_cache", MockDualCache())
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
setattr(
|
|
litellm.proxy.proxy_server,
|
|
"general_settings",
|
|
{"allow_requests_on_db_unavailable": True},
|
|
)
|
|
|
|
# Create test request
|
|
request = Request(scope={"type": "http"})
|
|
request._url = URL(url="/chat/completions")
|
|
|
|
# Run test with a sample API key
|
|
result = await user_api_key_auth(
|
|
request=request,
|
|
api_key="Bearer sk-123456789",
|
|
)
|
|
|
|
from litellm.proxy.auth.auth_exception_handler import (
|
|
DB_UNAVAILABLE_FALLBACK_USER_ID,
|
|
)
|
|
|
|
# Verify results. user_id is the non-admin fallback sentinel so a DB
|
|
# outage cannot escalate an anonymous caller to proxy-admin.
|
|
assert isinstance(result, UserAPIKeyAuth)
|
|
assert result.key_name == "failed-to-connect-to-db"
|
|
assert result.user_id == DB_UNAVAILABLE_FALLBACK_USER_ID
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_user_api_key_auth_db_unavailable_not_allowed():
|
|
"""
|
|
Test that user_api_key_auth raises an exception when:
|
|
This is default behavior
|
|
|
|
1. DB connection fails during token validation
|
|
2. allow_requests_on_db_unavailable=False (default behavior)
|
|
"""
|
|
|
|
# Mock dependencies
|
|
class MockPrismaClient:
|
|
async def get_data(self, *args, **kwargs):
|
|
print("MockPrismaClient.get_data() called")
|
|
raise httpx.ConnectError("Failed to connect to DB")
|
|
|
|
async def connect(self):
|
|
print("MockPrismaClient.connect() called")
|
|
pass
|
|
|
|
class MockDualCache:
|
|
async def async_get_cache(self, *args, **kwargs):
|
|
return None
|
|
|
|
async def async_set_cache(self, *args, **kwargs):
|
|
pass
|
|
|
|
async def set_cache(self, *args, **kwargs):
|
|
pass
|
|
|
|
# Set up test environment
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", MockPrismaClient())
|
|
setattr(litellm.proxy.proxy_server, "user_api_key_cache", MockDualCache())
|
|
setattr(litellm.proxy.proxy_server, "general_settings", {})
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
|
|
# Create test request
|
|
request = Request(scope={"type": "http"})
|
|
request._url = URL(url="/chat/completions")
|
|
|
|
# Run test with a sample API key
|
|
with pytest.raises(litellm.proxy._types.ProxyException):
|
|
await user_api_key_auth(
|
|
request=request,
|
|
api_key="Bearer sk-123456789",
|
|
)
|
|
|
|
|
|
## E2E Virtual Key + Secret Manager Tests #########################################
|
|
|
|
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
@pytest.mark.asyncio
|
|
@mock.patch(
|
|
"litellm.secret_managers.aws_secret_manager_v2.AWSSecretsManagerV2.async_write_secret"
|
|
)
|
|
@mock.patch(
|
|
"litellm.secret_managers.aws_secret_manager_v2.AWSSecretsManagerV2.async_read_secret"
|
|
)
|
|
@mock.patch(
|
|
"litellm.secret_managers.aws_secret_manager_v2.AWSSecretsManagerV2.async_delete_secret"
|
|
)
|
|
async def test_key_generate_with_secret_manager_call(
|
|
mock_delete_secret, mock_read_secret, mock_write_secret, prisma_client
|
|
):
|
|
"""
|
|
Generate a key
|
|
assert it exists in the secret manager
|
|
|
|
delete the key
|
|
assert it is deleted from the secret manager
|
|
"""
|
|
from litellm.secret_managers.aws_secret_manager_v2 import AWSSecretsManagerV2
|
|
from litellm.types.secret_managers.main import (
|
|
KeyManagementSystem,
|
|
KeyManagementSettings,
|
|
)
|
|
|
|
from litellm.proxy.hooks.key_management_event_hooks import (
|
|
LITELLM_PREFIX_STORED_VIRTUAL_KEYS,
|
|
)
|
|
|
|
litellm.set_verbose = True
|
|
|
|
#### Test Setup ############################################################
|
|
aws_secret_manager_client = AWSSecretsManagerV2()
|
|
litellm.secret_manager_client = aws_secret_manager_client
|
|
litellm._key_management_system = KeyManagementSystem.AWS_SECRET_MANAGER
|
|
litellm._key_management_settings = KeyManagementSettings(
|
|
store_virtual_keys=True,
|
|
)
|
|
general_settings = {
|
|
"key_management_system": "aws_secret_manager",
|
|
"key_management_settings": {
|
|
"store_virtual_keys": True,
|
|
},
|
|
}
|
|
|
|
setattr(litellm.proxy.proxy_server, "general_settings", general_settings)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
############################################################################
|
|
|
|
# generate new key
|
|
key_alias = f"test_alias_secret_manager_key-{uuid.uuid4()}"
|
|
spend = 100
|
|
max_budget = 400
|
|
models = ["fake-openai-endpoint"]
|
|
|
|
# Mock write_secret to return success
|
|
mock_write_secret.return_value = None
|
|
|
|
new_key = await generate_key_fn(
|
|
data=GenerateKeyRequest(
|
|
key_alias=key_alias, spend=spend, max_budget=max_budget, models=models
|
|
),
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
|
|
generated_key = new_key.key
|
|
print(generated_key)
|
|
|
|
await asyncio.sleep(2)
|
|
|
|
# read from the secret manager
|
|
# Mock read_secret to return the generated key
|
|
mock_read_secret.return_value = generated_key
|
|
|
|
result = await aws_secret_manager_client.async_read_secret(
|
|
secret_name=f"{litellm._key_management_settings.prefix_for_stored_virtual_keys}{key_alias}"
|
|
)
|
|
|
|
# Assert the correct key is stored in the secret manager
|
|
print("response from AWS Secret Manager")
|
|
print(result)
|
|
assert result == generated_key
|
|
|
|
# Mock delete_secret to return success
|
|
mock_delete_secret.return_value = None
|
|
|
|
# delete the key
|
|
await delete_key_fn(
|
|
data=KeyRequest(keys=[generated_key]),
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN, api_key="sk-1234", user_id="1234"
|
|
),
|
|
)
|
|
|
|
await asyncio.sleep(2)
|
|
|
|
# Assert the key is deleted from the secret manager
|
|
# Mock read_secret to return None after deletion
|
|
mock_read_secret.return_value = None
|
|
|
|
result = await aws_secret_manager_client.async_read_secret(
|
|
secret_name=f"{litellm._key_management_settings.prefix_for_stored_virtual_keys}{key_alias}"
|
|
)
|
|
assert result is None
|
|
|
|
# cleanup
|
|
setattr(litellm.proxy.proxy_server, "general_settings", {})
|
|
|
|
|
|
################################################################################
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
async def test_key_alias_uniqueness(prisma_client):
|
|
"""
|
|
Test that:
|
|
1. We cannot create two keys with the same alias
|
|
2. We cannot update a key to use an alias that's already taken
|
|
3. We can update a key while keeping its existing alias
|
|
"""
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
|
|
try:
|
|
# Create first key with an alias
|
|
unique_alias = f"test-alias-{uuid.uuid4()}"
|
|
key1 = await generate_key_fn(
|
|
data=GenerateKeyRequest(key_alias=unique_alias),
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
|
|
# Try to create second key with same alias - should fail
|
|
with pytest.raises(Exception, match="Unique key aliases across all keys are required") as exc_info:
|
|
key2 = await generate_key_fn(
|
|
data=GenerateKeyRequest(key_alias=unique_alias),
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
e = exc_info.value
|
|
print("vars(e)=", vars(e))
|
|
assert "Unique key aliases across all keys are required" in str(e.message)
|
|
|
|
# Create another key with different alias
|
|
another_alias = f"test-alias-{uuid.uuid4()}"
|
|
key3 = await generate_key_fn(
|
|
data=GenerateKeyRequest(key_alias=another_alias),
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
|
|
# Try to update key3 to use key1's alias - should fail
|
|
with pytest.raises(Exception, match="Unique key aliases across all keys are required") as exc_info:
|
|
await update_key_fn(
|
|
data=UpdateKeyRequest(key=key3.key, key_alias=unique_alias),
|
|
request=Request(scope={"type": "http"}),
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
e = exc_info.value
|
|
assert "Unique key aliases across all keys are required" in str(e.message)
|
|
|
|
# Update key1 with its own existing alias - should succeed
|
|
updated_key = await update_key_fn(
|
|
data=UpdateKeyRequest(key=key1.key, key_alias=unique_alias),
|
|
request=Request(scope={"type": "http"}),
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
assert updated_key is not None
|
|
|
|
except Exception as e:
|
|
print("got exceptions, e=", e)
|
|
print("vars(e)=", vars(e))
|
|
pytest.fail(f"An unexpected error occurred: {str(e)}")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
async def test_enforce_unique_key_alias(prisma_client):
|
|
"""
|
|
Unit test the _enforce_unique_key_alias function:
|
|
1. Test it allows unique aliases
|
|
2. Test it blocks duplicate aliases for new keys
|
|
3. Test it allows updating a key with its own existing alias
|
|
4. Test it blocks updating a key with another key's alias
|
|
"""
|
|
from litellm.proxy.management_endpoints.key_management_endpoints import (
|
|
_enforce_unique_key_alias,
|
|
)
|
|
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
|
|
try:
|
|
# Test 1: Allow unique alias
|
|
unique_alias = f"test-alias-{uuid.uuid4()}"
|
|
await _enforce_unique_key_alias(
|
|
key_alias=unique_alias,
|
|
prisma_client=prisma_client,
|
|
) # Should pass
|
|
|
|
# Create a key with this alias in the database
|
|
key1 = await generate_key_fn(
|
|
data=GenerateKeyRequest(key_alias=unique_alias),
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
|
|
# Test 2: Block duplicate alias for new key
|
|
with pytest.raises(Exception, match="Unique key aliases across all keys are required") as exc_info:
|
|
await _enforce_unique_key_alias(
|
|
key_alias=unique_alias,
|
|
prisma_client=prisma_client,
|
|
)
|
|
e = exc_info.value
|
|
assert "Unique key aliases across all keys are required" in str(e.message)
|
|
|
|
# Test 3: Allow updating key with its own alias
|
|
await _enforce_unique_key_alias(
|
|
key_alias=unique_alias,
|
|
existing_key_token=hash_token(key1.key),
|
|
prisma_client=prisma_client,
|
|
) # Should pass
|
|
|
|
# Test 4: Block updating with another key's alias
|
|
another_key = await generate_key_fn(
|
|
data=GenerateKeyRequest(key_alias=f"test-alias-{uuid.uuid4()}"),
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
|
|
with pytest.raises(Exception, match="Unique key aliases across all keys are required") as exc_info:
|
|
await _enforce_unique_key_alias(
|
|
key_alias=unique_alias,
|
|
existing_key_token=another_key.key,
|
|
prisma_client=prisma_client,
|
|
)
|
|
e = exc_info.value
|
|
assert "Unique key aliases across all keys are required" in str(e.message)
|
|
|
|
except Exception as e:
|
|
print("Unexpected error:", e)
|
|
pytest.fail(f"An unexpected error occurred: {str(e)}")
|
|
|
|
|
|
def test_should_track_cost_callback():
|
|
"""
|
|
Test that the should_track_cost_callback function works as expected
|
|
"""
|
|
from litellm.proxy.hooks.proxy_track_cost_callback import (
|
|
_should_track_cost_callback,
|
|
)
|
|
|
|
assert _should_track_cost_callback(
|
|
user_api_key=None,
|
|
user_id=None,
|
|
team_id=None,
|
|
end_user_id="1234",
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
async def test_get_paginated_teams(prisma_client):
|
|
"""
|
|
Test the get_paginated_teams function:
|
|
1. Test pagination returns valid results
|
|
2. Test total count matches across pages
|
|
3. Test page size is respected
|
|
"""
|
|
from litellm.proxy.management_endpoints.team_endpoints import get_paginated_teams
|
|
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
|
|
try:
|
|
# Get first page with page_size=2
|
|
teams_page_1, total_count_1 = await get_paginated_teams(
|
|
prisma_client=prisma_client, page_size=2, page=1
|
|
)
|
|
|
|
print("teams_page_1=", teams_page_1)
|
|
print("total_count_1=", total_count_1)
|
|
|
|
# Get second page
|
|
teams_page_2, total_count_2 = await get_paginated_teams(
|
|
prisma_client=prisma_client, page_size=2, page=2
|
|
)
|
|
|
|
print("teams_page_2=", teams_page_2)
|
|
print("total_count_2=", total_count_2)
|
|
|
|
# Verify results
|
|
assert isinstance(teams_page_1, list) # Should return a list
|
|
assert isinstance(total_count_1, int) # Should return an integer count
|
|
assert (
|
|
total_count_1 == total_count_2
|
|
) # Total count should be consistent across pages
|
|
assert len(teams_page_1) <= 2 # Should respect page_size limit
|
|
|
|
except Exception as e:
|
|
print(f"Error occurred: {e}")
|
|
pytest.fail(f"Test failed with exception: {e}")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.flaky(retries=3, delay=1)
|
|
@pytest.mark.parametrize("entity_type", ["key", "user", "team"])
|
|
@pytest.mark.skip(
|
|
reason="Skipping reset budget job test. Fails on ci/cd due to db timeout errors. Need to replace with mock db."
|
|
)
|
|
async def test_reset_budget_job(prisma_client, entity_type):
|
|
"""
|
|
Test that the ResetBudgetJob correctly resets budgets for keys, users, and teams.
|
|
|
|
For each entity type:
|
|
1. Create a new entity with max_budget=100, spend=99, budget_duration=5s
|
|
2. Call the reset_budget function
|
|
3. Verify the entity's spend is reset to 0 and budget_reset_at is updated
|
|
"""
|
|
from datetime import datetime, timedelta
|
|
import time
|
|
|
|
from litellm.proxy.common_utils.reset_budget_job import ResetBudgetJob
|
|
from litellm.proxy.utils import ProxyLogging
|
|
|
|
# Setup
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
|
|
proxy_logging_obj = ProxyLogging(user_api_key_cache=None)
|
|
reset_budget_job = ResetBudgetJob(
|
|
proxy_logging_obj=proxy_logging_obj, prisma_client=prisma_client
|
|
)
|
|
|
|
# Create entity based on type
|
|
entity_id = None
|
|
if entity_type == "key":
|
|
# Create a key with specific budget settings
|
|
key = await generate_key_fn(
|
|
data=GenerateKeyRequest(
|
|
max_budget=100,
|
|
budget_duration="5s",
|
|
),
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
entity_id = key.token_id
|
|
print("generated key=", key)
|
|
|
|
# Update the key to set spend and reset_at to now
|
|
updated = await prisma_client.db.litellm_verificationtoken.update_many(
|
|
where={"token": key.token_id},
|
|
data={
|
|
"spend": 99.0,
|
|
},
|
|
)
|
|
print("Updated key=", updated)
|
|
|
|
elif entity_type == "user":
|
|
# Create a user with specific budget settings
|
|
user = await new_user(
|
|
data=NewUserRequest(
|
|
max_budget=100,
|
|
budget_duration="5s",
|
|
),
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
)
|
|
entity_id = user.user_id
|
|
|
|
# Update the user to set spend and reset_at to now
|
|
await prisma_client.db.litellm_usertable.update_many(
|
|
where={"user_id": user.user_id},
|
|
data={
|
|
"spend": 99.0,
|
|
},
|
|
)
|
|
|
|
elif entity_type == "team":
|
|
# Create a team with specific budget settings
|
|
team_id = f"test-team-{uuid.uuid4()}"
|
|
team = await new_team(
|
|
NewTeamRequest(
|
|
team_id=team_id,
|
|
max_budget=100,
|
|
budget_duration="5s",
|
|
),
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
user_role=LitellmUserRoles.PROXY_ADMIN,
|
|
api_key="sk-1234",
|
|
user_id="1234",
|
|
),
|
|
http_request=Request(scope={"type": "http"}),
|
|
)
|
|
entity_id = team_id
|
|
|
|
# Update the team to set spend and reset_at to now
|
|
current_time = datetime.utcnow()
|
|
await prisma_client.db.litellm_teamtable.update(
|
|
where={"team_id": team_id},
|
|
data={
|
|
"spend": 99.0,
|
|
},
|
|
)
|
|
|
|
# Verify entity was created and updated with spend
|
|
if entity_type == "key":
|
|
entity_before = await prisma_client.db.litellm_verificationtoken.find_unique(
|
|
where={"token": entity_id}
|
|
)
|
|
elif entity_type == "user":
|
|
entity_before = await prisma_client.db.litellm_usertable.find_unique(
|
|
where={"user_id": entity_id}
|
|
)
|
|
elif entity_type == "team":
|
|
entity_before = await prisma_client.db.litellm_teamtable.find_unique(
|
|
where={"team_id": entity_id}
|
|
)
|
|
|
|
assert entity_before is not None
|
|
assert entity_before.spend == 99.0
|
|
|
|
# Wait for 5 seconds to pass
|
|
print("sleeping for 5 seconds")
|
|
time.sleep(5)
|
|
|
|
# Call the reset_budget function
|
|
await reset_budget_job.reset_budget()
|
|
|
|
# Verify the entity's spend is reset and budget_reset_at is updated
|
|
if entity_type == "key":
|
|
entity_after = await prisma_client.db.litellm_verificationtoken.find_unique(
|
|
where={"token": entity_id}
|
|
)
|
|
elif entity_type == "user":
|
|
entity_after = await prisma_client.db.litellm_usertable.find_unique(
|
|
where={"user_id": entity_id}
|
|
)
|
|
elif entity_type == "team":
|
|
entity_after = await prisma_client.db.litellm_teamtable.find_unique(
|
|
where={"team_id": entity_id}
|
|
)
|
|
|
|
assert entity_after is not None
|
|
assert entity_after.spend == 0.0
|
|
|
|
|
|
@pytest.mark.skip(reason="Requires reliable external DB connection (prisma).")
|
|
def test_delete_nonexistent_key_returns_404(prisma_client):
|
|
# Try to delete a key that does not exist, expect a 404 error
|
|
import random, string
|
|
from litellm.proxy._types import (
|
|
KeyRequest,
|
|
UserAPIKeyAuth,
|
|
LitellmUserRoles,
|
|
ProxyException,
|
|
)
|
|
from litellm.proxy.management_endpoints.key_management_endpoints import (
|
|
delete_key_fn,
|
|
)
|
|
from starlette.datastructures import URL
|
|
from fastapi import Request
|
|
|
|
print("prisma client=", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "prisma_client", prisma_client)
|
|
setattr(litellm.proxy.proxy_server, "master_key", "sk-1234")
|
|
try:
|
|
|
|
async def test():
|
|
await litellm.proxy.proxy_server.prisma_client.connect()
|
|
# Generate a random key that does not exist
|
|
random_key = "sk-" + "".join(
|
|
random.choices(string.ascii_letters + string.digits, k=24)
|
|
)
|
|
delete_key_request = KeyRequest(keys=[random_key])
|
|
bearer_token = "Bearer sk-1234"
|
|
request = Request(scope={"type": "http"})
|
|
request._url = URL(url="/key/delete")
|
|
# use admin to auth in
|
|
result = await litellm.proxy.proxy_server.user_api_key_auth(
|
|
request=request, api_key=bearer_token
|
|
)
|
|
result.user_role = LitellmUserRoles.PROXY_ADMIN
|
|
with pytest.raises(ProxyException) as exc_info:
|
|
await delete_key_fn(data=delete_key_request, user_api_key_dict=result)
|
|
e = exc_info.value
|
|
print("Caught ProxyException:", e)
|
|
assert str(e.code) == "404"
|
|
assert "No keys found" in str(
|
|
e.message
|
|
) or "No matching keys or aliases found to delete" in str(e.message)
|
|
|
|
import asyncio
|
|
|
|
asyncio.run(test())
|
|
except Exception as e:
|
|
pytest.fail(f"An exception occurred - {str(e)}")
|