mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
A test that asserts on the error inside its own except block passes when the call stops raising, because nothing runs the handler. That is the exact case the test exists to catch, so the regression lands green. Rewrites all 111 such blocks into pytest.raises, which fails when the call succeeds, and selects PT017 in ruff-tests.toml so no new one lands.
889 lines
30 KiB
Python
889 lines
30 KiB
Python
# What this tests ?
|
|
## Tests /key endpoints.
|
|
|
|
import pytest
|
|
import asyncio, uuid
|
|
import aiohttp
|
|
from openai import AsyncOpenAI
|
|
import sys, os
|
|
from typing import Optional
|
|
|
|
sys.path.insert(
|
|
0, os.path.abspath("../")
|
|
) # Adds the parent directory to the system path
|
|
import litellm
|
|
from litellm.proxy._types import LitellmUserRoles
|
|
|
|
|
|
async def generate_team(
|
|
session, models: Optional[list] = None, team_id: Optional[str] = None
|
|
):
|
|
url = "http://0.0.0.0:4000/team/new"
|
|
headers = {"Authorization": "Bearer sk-1234", "Content-Type": "application/json"}
|
|
if team_id is None:
|
|
team_id = "litellm-dashboard"
|
|
data = {"team_id": team_id, "models": models}
|
|
|
|
async with session.post(url, headers=headers, json=data) as response:
|
|
status = response.status
|
|
response_text = await response.text()
|
|
|
|
print(f"Response (Status code: {status}):")
|
|
print(response_text)
|
|
print()
|
|
_json_response = await response.json()
|
|
return _json_response
|
|
|
|
|
|
async def generate_user(
|
|
session,
|
|
user_role="app_owner",
|
|
):
|
|
url = "http://0.0.0.0:4000/user/new"
|
|
headers = {"Authorization": "Bearer sk-1234", "Content-Type": "application/json"}
|
|
data = {
|
|
"user_role": user_role,
|
|
"team_id": "litellm-dashboard",
|
|
}
|
|
|
|
async with session.post(url, headers=headers, json=data) as response:
|
|
status = response.status
|
|
response_text = await response.text()
|
|
|
|
print(f"Response (Status code: {status}):")
|
|
print(response_text)
|
|
print()
|
|
_json_response = await response.json()
|
|
return _json_response
|
|
|
|
|
|
async def generate_key(
|
|
session,
|
|
i,
|
|
budget=None,
|
|
budget_duration=None,
|
|
models=["azure-models", "gpt-4", "gpt-image-1"],
|
|
max_parallel_requests: Optional[int] = None,
|
|
user_id: Optional[str] = None,
|
|
team_id: Optional[str] = None,
|
|
metadata: Optional[dict] = None,
|
|
calling_key="sk-1234",
|
|
):
|
|
url = "http://0.0.0.0:4000/key/generate"
|
|
headers = {
|
|
"Authorization": f"Bearer {calling_key}",
|
|
"Content-Type": "application/json",
|
|
}
|
|
data = {
|
|
"models": models,
|
|
"aliases": {"mistral-7b": "gpt-3.5-turbo"},
|
|
"duration": None,
|
|
"max_budget": budget,
|
|
"budget_duration": budget_duration,
|
|
"max_parallel_requests": max_parallel_requests,
|
|
"user_id": user_id,
|
|
"team_id": team_id,
|
|
"metadata": metadata,
|
|
}
|
|
|
|
print(f"data: {data}")
|
|
|
|
async with session.post(url, headers=headers, json=data) as response:
|
|
status = response.status
|
|
response_text = await response.text()
|
|
|
|
print(f"Response {i} (Status code: {status}):")
|
|
print(response_text)
|
|
print()
|
|
|
|
if status != 200:
|
|
raise Exception(f"Request {i} did not return a 200 status code: {status}")
|
|
|
|
return await response.json()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_key_gen():
|
|
async with aiohttp.ClientSession() as session:
|
|
tasks = [generate_key(session, i) for i in range(1, 11)]
|
|
await asyncio.gather(*tasks)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_simple_key_gen():
|
|
async with aiohttp.ClientSession() as session:
|
|
key_data = await generate_key(session, i=0)
|
|
key = key_data["key"]
|
|
assert key_data["token"] is not None
|
|
assert key_data["token"] != key
|
|
assert key_data["token_id"] is not None
|
|
assert key_data["created_at"] is not None
|
|
assert key_data["updated_at"] is not None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_key_gen_bad_key():
|
|
"""
|
|
Test if you can create a key with a non-admin key, even with UI setup
|
|
"""
|
|
async with aiohttp.ClientSession() as session:
|
|
## LOGIN TO UI
|
|
form_data = {"username": "admin", "password": "sk-1234"}
|
|
async with session.post(
|
|
"http://0.0.0.0:4000/login", data=form_data
|
|
) as response:
|
|
assert (
|
|
response.status == 200
|
|
) # Assuming the endpoint returns a 500 status code for error handling
|
|
text = await response.text()
|
|
print(text)
|
|
## create user key with admin key -> expect to work
|
|
key_data = await generate_key(session=session, i=0, user_id="user-1234")
|
|
key = key_data["key"]
|
|
## create new key with user key -> expect to fail
|
|
try:
|
|
await generate_key(
|
|
session=session, i=0, user_id="user-1234", calling_key=key
|
|
)
|
|
pytest.fail("Expected to fail")
|
|
except Exception as e:
|
|
pass
|
|
|
|
|
|
async def update_key(session, get_key, metadata: Optional[dict] = None):
|
|
"""
|
|
Make sure only models user has access to are returned
|
|
"""
|
|
url = "http://0.0.0.0:4000/key/update"
|
|
headers = {
|
|
"Authorization": "Bearer sk-1234",
|
|
"Content-Type": "application/json",
|
|
}
|
|
data = {"key": get_key}
|
|
|
|
if metadata is not None:
|
|
data["metadata"] = metadata
|
|
else:
|
|
data.update({"models": ["gpt-4"], "duration": "120s"})
|
|
|
|
async with session.post(url, headers=headers, json=data) as response:
|
|
status = response.status
|
|
response_text = await response.text()
|
|
print(response_text)
|
|
print()
|
|
|
|
if status != 200:
|
|
raise Exception(f"Request did not return a 200 status code: {status}")
|
|
return await response.json()
|
|
|
|
|
|
async def update_proxy_budget(session):
|
|
"""
|
|
Make sure only models user has access to are returned
|
|
"""
|
|
url = "http://0.0.0.0:4000/user/update"
|
|
headers = {
|
|
"Authorization": f"Bearer sk-1234",
|
|
"Content-Type": "application/json",
|
|
}
|
|
data = {"user_id": "litellm-proxy-budget", "spend": 0}
|
|
|
|
async with session.post(url, headers=headers, json=data) as response:
|
|
status = response.status
|
|
response_text = await response.text()
|
|
print(response_text)
|
|
print()
|
|
|
|
if status != 200:
|
|
raise Exception(f"Request did not return a 200 status code: {status}")
|
|
return await response.json()
|
|
|
|
|
|
async def chat_completion(session, key, model="gpt-4"):
|
|
url = "http://0.0.0.0:4000/chat/completions"
|
|
headers = {
|
|
"Authorization": f"Bearer {key}",
|
|
"Content-Type": "application/json",
|
|
}
|
|
data = {
|
|
"model": model,
|
|
"messages": [
|
|
{"role": "system", "content": "You are a helpful assistant."},
|
|
{"role": "user", "content": "Hello!"},
|
|
],
|
|
}
|
|
|
|
for i in range(3):
|
|
try:
|
|
async with session.post(url, headers=headers, json=data) as response:
|
|
status = response.status
|
|
response_text = await response.text()
|
|
|
|
print(response_text)
|
|
print()
|
|
|
|
if status != 200:
|
|
raise Exception(
|
|
f"Request did not return a 200 status code: {status}. Response: {response_text}"
|
|
)
|
|
|
|
return await response.json()
|
|
except Exception as e:
|
|
if "Request did not return a 200 status code" in str(e):
|
|
raise e
|
|
else:
|
|
pass
|
|
|
|
|
|
async def image_generation(session, key, model="gpt-image-1"):
|
|
url = "http://0.0.0.0:4000/v1/images/generations"
|
|
headers = {
|
|
"Authorization": f"Bearer {key}",
|
|
"Content-Type": "application/json",
|
|
}
|
|
data = {
|
|
"model": model,
|
|
"prompt": "A cute baby sea otter",
|
|
}
|
|
|
|
for i in range(3):
|
|
try:
|
|
async with session.post(url, headers=headers, json=data) as response:
|
|
status = response.status
|
|
response_text = await response.text()
|
|
print("/images/generations response", response_text)
|
|
|
|
print()
|
|
|
|
if status != 200:
|
|
raise Exception(
|
|
f"Request did not return a 200 status code: {status}. Response: {response_text}"
|
|
)
|
|
|
|
return await response.json()
|
|
except Exception as e:
|
|
if "Request did not return a 200 status code" in str(e):
|
|
raise e
|
|
else:
|
|
pass
|
|
|
|
|
|
async def chat_completion_streaming(session, key, model="gpt-4"):
|
|
client = AsyncOpenAI(api_key=key, base_url="http://0.0.0.0:4000")
|
|
messages = [
|
|
{"role": "system", "content": "You are a helpful assistant"},
|
|
{"role": "user", "content": "Hello!"},
|
|
]
|
|
prompt_tokens = litellm.token_counter(model="gpt-35-turbo", messages=messages)
|
|
data = {
|
|
"model": model,
|
|
"messages": messages,
|
|
"stream": True,
|
|
}
|
|
response = await client.chat.completions.create(**data)
|
|
|
|
content = ""
|
|
async for chunk in response:
|
|
content += chunk.choices[0].delta.content or ""
|
|
|
|
print(f"content: {content}")
|
|
|
|
completion_tokens = litellm.token_counter(
|
|
model="gpt-35-turbo", text=content, count_response_tokens=True
|
|
)
|
|
|
|
return prompt_tokens, completion_tokens
|
|
|
|
|
|
@pytest.mark.parametrize("metadata", [{"test": "new"}, {}])
|
|
@pytest.mark.asyncio
|
|
async def test_key_update(metadata):
|
|
"""
|
|
Create key
|
|
Update key with new model
|
|
Test key w/ model
|
|
"""
|
|
async with aiohttp.ClientSession() as session:
|
|
key_gen = await generate_key(session=session, i=0, metadata={"test": "test"})
|
|
key = key_gen["key"]
|
|
assert key_gen["metadata"]["test"] == "test"
|
|
updated_key = await update_key(
|
|
session=session,
|
|
get_key=key,
|
|
metadata=metadata,
|
|
)
|
|
print(f"updated_key['metadata']: {updated_key['metadata']}")
|
|
assert updated_key["metadata"] == metadata
|
|
await update_proxy_budget(session=session) # resets proxy spend
|
|
await chat_completion(session=session, key=key)
|
|
|
|
|
|
async def delete_key(session, get_key, auth_key="sk-1234"):
|
|
"""
|
|
Delete key
|
|
"""
|
|
url = "http://0.0.0.0:4000/key/delete"
|
|
headers = {
|
|
"Authorization": f"Bearer {auth_key}",
|
|
"Content-Type": "application/json",
|
|
}
|
|
data = {"keys": [get_key]}
|
|
|
|
async with session.post(url, headers=headers, json=data) as response:
|
|
status = response.status
|
|
response_text = await response.text()
|
|
print(response_text)
|
|
print()
|
|
|
|
if status != 200:
|
|
raise Exception(f"Request did not return a 200 status code: {status}")
|
|
return await response.json()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_key_delete():
|
|
"""
|
|
Delete key
|
|
"""
|
|
async with aiohttp.ClientSession() as session:
|
|
key_gen = await generate_key(session=session, i=0)
|
|
key = key_gen["key"]
|
|
await delete_key(
|
|
session=session,
|
|
get_key=key,
|
|
)
|
|
|
|
|
|
async def get_key_info(session, call_key, get_key=None):
|
|
"""
|
|
Make sure only models user has access to are returned
|
|
"""
|
|
if get_key is None:
|
|
url = "http://0.0.0.0:4000/key/info"
|
|
else:
|
|
url = f"http://0.0.0.0:4000/key/info?key={get_key}"
|
|
headers = {
|
|
"Authorization": f"Bearer {call_key}",
|
|
"Content-Type": "application/json",
|
|
}
|
|
|
|
async with session.get(url, headers=headers) as response:
|
|
status = response.status
|
|
response_text = await response.text()
|
|
print(response_text)
|
|
print()
|
|
|
|
if status != 200:
|
|
if call_key != get_key:
|
|
return status
|
|
else:
|
|
print(f"call_key: {call_key}; get_key: {get_key}")
|
|
raise Exception(
|
|
f"Request did not return a 200 status code: {status}. Responses {response_text}"
|
|
)
|
|
return await response.json()
|
|
|
|
|
|
async def get_model_list(session, call_key, endpoint: str = "/v1/models"):
|
|
"""
|
|
Make sure only models user has access to are returned
|
|
"""
|
|
url = "http://0.0.0.0:4000" + endpoint
|
|
headers = {
|
|
"Authorization": f"Bearer {call_key}",
|
|
"Content-Type": "application/json",
|
|
}
|
|
|
|
async with session.get(url, headers=headers) as response:
|
|
status = response.status
|
|
response_text = await response.text()
|
|
print(response_text)
|
|
print()
|
|
|
|
if status != 200:
|
|
raise Exception(
|
|
f"Request did not return a 200 status code: {status}. Responses {response_text}"
|
|
)
|
|
return await response.json()
|
|
|
|
|
|
async def get_model_info(session, call_key):
|
|
"""
|
|
Make sure only models user has access to are returned
|
|
"""
|
|
url = "http://0.0.0.0:4000/model/info"
|
|
headers = {
|
|
"Authorization": f"Bearer {call_key}",
|
|
"Content-Type": "application/json",
|
|
}
|
|
|
|
async with session.get(url, headers=headers) as response:
|
|
status = response.status
|
|
response_text = await response.text()
|
|
print(response_text)
|
|
print()
|
|
|
|
if status != 200:
|
|
raise Exception(
|
|
f"Request did not return a 200 status code: {status}. Responses {response_text}"
|
|
)
|
|
return await response.json()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_key_info():
|
|
"""
|
|
Get key info
|
|
- as admin -> 200
|
|
- as key itself -> 200
|
|
- as non existent key -> 404
|
|
"""
|
|
async with aiohttp.ClientSession() as session:
|
|
key_gen = await generate_key(session=session, i=0)
|
|
key = key_gen["key"]
|
|
# as admin #
|
|
await get_key_info(session=session, get_key=key, call_key="sk-1234")
|
|
# as key itself #
|
|
await get_key_info(session=session, get_key=key, call_key=key)
|
|
|
|
# as key itself, use the auth param, and no query key needed
|
|
await get_key_info(session=session, call_key=key)
|
|
# as random key #
|
|
random_key = f"sk-{uuid.uuid4()}"
|
|
status = await get_key_info(session=session, get_key=random_key, call_key=key)
|
|
assert status == 404
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_model_info():
|
|
"""
|
|
Get model info for models key has access to
|
|
"""
|
|
async with aiohttp.ClientSession() as session:
|
|
key_gen = await generate_key(session=session, i=0)
|
|
key = key_gen["key"]
|
|
# as admin #
|
|
admin_models = await get_model_info(session=session, call_key="sk-1234")
|
|
admin_models = admin_models["data"]
|
|
# as key itself #
|
|
user_models = await get_model_info(session=session, call_key=key)
|
|
user_models = user_models["data"]
|
|
|
|
assert len(admin_models) > len(user_models)
|
|
assert len(user_models) > 0
|
|
|
|
|
|
async def get_spend_logs(session, request_id):
|
|
url = f"http://0.0.0.0:4000/spend/logs?request_id={request_id}"
|
|
headers = {"Authorization": "Bearer sk-1234", "Content-Type": "application/json"}
|
|
|
|
async with session.get(url, headers=headers) as response:
|
|
status = response.status
|
|
response_text = await response.text()
|
|
|
|
print(response_text)
|
|
print()
|
|
|
|
if status != 200:
|
|
raise Exception(f"Request did not return a 200 status code: {status}")
|
|
return await response.json()
|
|
|
|
|
|
@pytest.mark.skip(reason="Hanging on ci/cd")
|
|
@pytest.mark.asyncio
|
|
async def test_key_info_spend_values():
|
|
"""
|
|
Test to ensure spend is correctly calculated
|
|
- create key
|
|
- make completion call
|
|
- assert cost is expected value
|
|
"""
|
|
|
|
async def retry_request(func, *args, _max_attempts=5, **kwargs):
|
|
for attempt in range(_max_attempts):
|
|
try:
|
|
return await func(*args, **kwargs)
|
|
except aiohttp.client_exceptions.ClientOSError as e:
|
|
if attempt + 1 == _max_attempts:
|
|
raise # re-raise the last ClientOSError if all attempts failed
|
|
print(f"Attempt {attempt+1} failed, retrying...")
|
|
|
|
async with aiohttp.ClientSession() as session:
|
|
## Test Spend Update ##
|
|
# completion
|
|
key_gen = await generate_key(session=session, i=0)
|
|
key = key_gen["key"]
|
|
response = await chat_completion(session=session, key=key)
|
|
await asyncio.sleep(5)
|
|
spend_logs = await retry_request(
|
|
get_spend_logs, session=session, request_id=response["id"]
|
|
)
|
|
print(f"spend_logs: {spend_logs}")
|
|
completion_tokens = spend_logs[0]["completion_tokens"]
|
|
prompt_tokens = spend_logs[0]["prompt_tokens"]
|
|
print(f"prompt_tokens: {prompt_tokens}; completion_tokens: {completion_tokens}")
|
|
|
|
litellm.set_verbose = True
|
|
prompt_cost, completion_cost = litellm.cost_per_token(
|
|
model="gpt-35-turbo",
|
|
prompt_tokens=prompt_tokens,
|
|
completion_tokens=completion_tokens,
|
|
custom_llm_provider="azure",
|
|
)
|
|
print("prompt_cost: ", prompt_cost, "completion_cost: ", completion_cost)
|
|
response_cost = prompt_cost + completion_cost
|
|
print(f"response_cost: {response_cost}")
|
|
await asyncio.sleep(5) # allow db log to be updated
|
|
key_info = await get_key_info(session=session, get_key=key, call_key=key)
|
|
print(
|
|
f"response_cost: {response_cost}; key_info spend: {key_info['info']['spend']}"
|
|
)
|
|
rounded_response_cost = round(response_cost, 8)
|
|
rounded_key_info_spend = round(key_info["info"]["spend"], 8)
|
|
assert (
|
|
rounded_response_cost == rounded_key_info_spend
|
|
), f"Expected cost= {rounded_response_cost} != Tracked Cost={rounded_key_info_spend}"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.flaky(retries=6, delay=2)
|
|
@pytest.mark.skip(
|
|
reason="Temporarily skipping due to model change. Will be updated soon."
|
|
)
|
|
async def test_aaaaakey_info_spend_values_streaming():
|
|
"""
|
|
Test to ensure spend is correctly calculated.
|
|
- create key
|
|
- make completion call
|
|
- assert cost is expected value
|
|
"""
|
|
async with aiohttp.ClientSession() as session:
|
|
## streaming - azure
|
|
key_gen = await generate_key(session=session, i=0)
|
|
new_key = key_gen["key"]
|
|
prompt_tokens, completion_tokens = await chat_completion_streaming(
|
|
session=session, key=new_key
|
|
)
|
|
print(f"prompt_tokens: {prompt_tokens}, completion_tokens: {completion_tokens}")
|
|
prompt_cost, completion_cost = litellm.cost_per_token(
|
|
model="azure/gpt-4o",
|
|
prompt_tokens=prompt_tokens,
|
|
completion_tokens=completion_tokens,
|
|
)
|
|
response_cost = prompt_cost + completion_cost
|
|
await asyncio.sleep(8) # allow db log to be updated
|
|
print(f"new_key: {new_key}")
|
|
key_info = await get_key_info(
|
|
session=session, get_key=new_key, call_key=new_key
|
|
)
|
|
print(
|
|
f"response_cost: {response_cost}; key_info spend: {key_info['info']['spend']}"
|
|
)
|
|
rounded_response_cost = round(response_cost, 8)
|
|
rounded_key_info_spend = round(key_info["info"]["spend"], 8)
|
|
assert (
|
|
rounded_response_cost == rounded_key_info_spend
|
|
), f"Expected={rounded_response_cost}, Got={rounded_key_info_spend}"
|
|
|
|
|
|
@pytest.mark.flaky(retries=3, delay=1)
|
|
@pytest.mark.asyncio
|
|
async def test_key_info_spend_values_image_generation():
|
|
"""
|
|
Test to ensure spend is correctly calculated
|
|
- create key
|
|
- make image gen call
|
|
- assert cost is expected value
|
|
"""
|
|
|
|
async def retry_request(func, *args, _max_attempts=5, **kwargs):
|
|
for attempt in range(_max_attempts):
|
|
try:
|
|
return await func(*args, **kwargs)
|
|
except aiohttp.client_exceptions.ClientOSError as e:
|
|
if attempt + 1 == _max_attempts:
|
|
raise # re-raise the last ClientOSError if all attempts failed
|
|
print(f"Attempt {attempt+1} failed, retrying...")
|
|
|
|
async with aiohttp.ClientSession(
|
|
timeout=aiohttp.ClientTimeout(total=600)
|
|
) as session:
|
|
## Test Spend Update ##
|
|
# completion
|
|
key_gen = await generate_key(session=session, i=0)
|
|
key = key_gen["key"]
|
|
response = await image_generation(session=session, key=key)
|
|
await asyncio.sleep(5)
|
|
key_info = await retry_request(
|
|
get_key_info, session=session, get_key=key, call_key=key
|
|
)
|
|
spend = key_info["info"]["spend"]
|
|
assert spend > 0
|
|
|
|
# The record/replay proxy serves this identical second call from its
|
|
# cassette (free), but the proxy must still bill it. Spend logging is
|
|
# async/batched, so poll for the increase rather than reading once after a
|
|
# fixed sleep; a spend that never grows means the repeat was not billed
|
|
# (e.g. the proxy response cache is on), which this still catches.
|
|
await image_generation(session=session, key=key)
|
|
spend_after = spend
|
|
for _ in range(12):
|
|
await asyncio.sleep(5)
|
|
key_info = await retry_request(
|
|
get_key_info, session=session, get_key=key, call_key=key
|
|
)
|
|
spend_after = key_info["info"]["spend"]
|
|
if spend_after > spend:
|
|
break
|
|
assert spend_after > spend, (
|
|
"spend did not increase on an identical repeat image call; the repeat "
|
|
"was not billed (the proxy response cache may be on)"
|
|
)
|
|
|
|
|
|
@pytest.mark.skip(reason="Frequent check on ci/cd leads to read timeout issue.")
|
|
@pytest.mark.asyncio
|
|
async def test_key_with_budgets():
|
|
"""
|
|
- Create key with budget and 5min duration
|
|
- Get 'reset_at' value
|
|
- wait 10min (budget reset runs every 10mins.)
|
|
- Check if value updated
|
|
"""
|
|
from litellm.proxy.utils import hash_token
|
|
|
|
async def retry_request(func, *args, _max_attempts=5, **kwargs):
|
|
for attempt in range(_max_attempts):
|
|
try:
|
|
return await func(*args, **kwargs)
|
|
except aiohttp.client_exceptions.ClientOSError as e:
|
|
if attempt + 1 == _max_attempts:
|
|
raise # re-raise the last ClientOSError if all attempts failed
|
|
print(f"Attempt {attempt+1} failed, retrying...")
|
|
|
|
async with aiohttp.ClientSession() as session:
|
|
key_gen = await generate_key(
|
|
session=session, i=0, budget=10, budget_duration="5s"
|
|
)
|
|
key = key_gen["key"]
|
|
hashed_token = hash_token(token=key)
|
|
print(f"hashed_token: {hashed_token}")
|
|
key_info = await get_key_info(session=session, get_key=key, call_key=key)
|
|
reset_at_init_value = key_info["info"]["budget_reset_at"]
|
|
reset_at_new_value = None
|
|
i = 0
|
|
for i in range(3):
|
|
await asyncio.sleep(70)
|
|
key_info = await retry_request(
|
|
get_key_info, session=session, get_key=key, call_key=key
|
|
)
|
|
reset_at_new_value = key_info["info"]["budget_reset_at"]
|
|
try:
|
|
assert reset_at_init_value != reset_at_new_value
|
|
break
|
|
except Exception:
|
|
i + 1
|
|
await asyncio.sleep(10)
|
|
assert reset_at_init_value != reset_at_new_value
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_key_crossing_budget():
|
|
"""
|
|
- Create key with budget with budget=0.00000001
|
|
- make a /chat/completions call
|
|
- wait 5s
|
|
- make a /chat/completions call - should fail with key crossed it's budget
|
|
|
|
- Check if value updated
|
|
"""
|
|
from litellm.proxy.utils import hash_token
|
|
|
|
async with aiohttp.ClientSession() as session:
|
|
key_gen = await generate_key(session=session, i=0, budget=0.0000001)
|
|
key = key_gen["key"]
|
|
hashed_token = hash_token(token=key)
|
|
print(f"hashed_token: {hashed_token}")
|
|
|
|
response = await chat_completion(session=session, key=key)
|
|
print("response 1: ", response)
|
|
await asyncio.sleep(10)
|
|
with pytest.raises(Exception, match="Budget has been exceeded!") as exc_info:
|
|
response = await chat_completion(session=session, key=key)
|
|
e = exc_info.value
|
|
assert "Budget has been exceeded!" in str(e)
|
|
|
|
|
|
@pytest.mark.skip(reason="AWS Suspended Account")
|
|
@pytest.mark.asyncio
|
|
async def test_key_info_spend_values_sagemaker():
|
|
"""
|
|
Tests the sync streaming loop to ensure spend is correctly calculated.
|
|
- create key
|
|
- make completion call
|
|
- assert cost is expected value
|
|
"""
|
|
async with aiohttp.ClientSession() as session:
|
|
## streaming - sagemaker
|
|
key_gen = await generate_key(session=session, i=0, models=[])
|
|
new_key = key_gen["key"]
|
|
prompt_tokens, completion_tokens = await chat_completion_streaming(
|
|
session=session, key=new_key, model="sagemaker-completion-model"
|
|
)
|
|
await asyncio.sleep(5) # allow db log to be updated
|
|
key_info = await get_key_info(
|
|
session=session, get_key=new_key, call_key=new_key
|
|
)
|
|
rounded_key_info_spend = round(key_info["info"]["spend"], 8)
|
|
assert rounded_key_info_spend > 0
|
|
# assert rounded_response_cost == rounded_key_info_spend
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_key_rate_limit():
|
|
"""
|
|
Tests backoff/retry logic on parallel request error.
|
|
- Create key with max parallel requests 0
|
|
- run 2 requests -> both fail
|
|
- Create key with max parallel request 1
|
|
- run 2 requests
|
|
- both should succeed
|
|
"""
|
|
async with aiohttp.ClientSession() as session:
|
|
key_gen = await generate_key(session=session, i=0, max_parallel_requests=0)
|
|
new_key = key_gen["key"]
|
|
try:
|
|
await chat_completion(session=session, key=new_key)
|
|
pytest.fail(f"Expected this call to fail")
|
|
except Exception as e:
|
|
pass
|
|
key_gen = await generate_key(session=session, i=0, max_parallel_requests=1)
|
|
new_key = key_gen["key"]
|
|
try:
|
|
await chat_completion(session=session, key=new_key)
|
|
except Exception as e:
|
|
pytest.fail(f"Expected this call to work - {str(e)}")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_key_delete_ui():
|
|
"""
|
|
Admin UI flow - DO NOT DELETE
|
|
-> Create a key with user_id = "ishaan"
|
|
-> Log on Admin UI, delete the key for user "ishaan"
|
|
-> This should work, since we're on the admin UI and role == "proxy_admin
|
|
"""
|
|
async with aiohttp.ClientSession() as session:
|
|
key_gen = await generate_key(session=session, i=0, user_id="ishaan-smart")
|
|
key = key_gen["key"]
|
|
|
|
# generate a admin UI key
|
|
team = await generate_team(session=session)
|
|
admin_ui_key = await generate_user(
|
|
session=session, user_role=LitellmUserRoles.PROXY_ADMIN.value
|
|
)
|
|
print(
|
|
"trying to delete key=",
|
|
key,
|
|
"using key=",
|
|
admin_ui_key["key"],
|
|
" to auth in",
|
|
)
|
|
|
|
await delete_key(
|
|
session=session,
|
|
get_key=key,
|
|
auth_key=admin_ui_key["key"],
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("model_access", ["all-team-models", "gpt-3.5-turbo"])
|
|
@pytest.mark.parametrize("model_access_level", ["key", "team"])
|
|
@pytest.mark.parametrize("model_endpoint", ["/v1/models", "/model/info"])
|
|
@pytest.mark.asyncio
|
|
async def test_key_model_list(model_access, model_access_level, model_endpoint):
|
|
"""
|
|
Test if `/v1/models` works as expected.
|
|
"""
|
|
async with aiohttp.ClientSession() as session:
|
|
_models = [] if model_access == "all-team-models" else [model_access]
|
|
team_id = "litellm_dashboard_{}".format(uuid.uuid4())
|
|
new_team = await generate_team(
|
|
session=session,
|
|
models=_models if model_access_level == "team" else None,
|
|
team_id=team_id,
|
|
)
|
|
key_gen = await generate_key(
|
|
session=session,
|
|
i=0,
|
|
team_id=team_id,
|
|
models=_models if model_access_level == "key" else [],
|
|
)
|
|
key = key_gen["key"]
|
|
print(f"key: {key}")
|
|
|
|
model_list = await get_model_list(
|
|
session=session, call_key=key, endpoint=model_endpoint
|
|
)
|
|
print(f"model_list: {model_list}")
|
|
|
|
if model_access == "all-team-models":
|
|
if model_endpoint == "/v1/models":
|
|
assert not isinstance(model_list["data"][0]["id"], list)
|
|
assert isinstance(model_list["data"][0]["id"], str)
|
|
elif model_endpoint == "/model/info":
|
|
assert isinstance(model_list["data"], list)
|
|
assert len(model_list["data"]) > 0
|
|
if model_access == "gpt-3.5-turbo":
|
|
if model_endpoint == "/v1/models":
|
|
assert (
|
|
len(model_list["data"]) == 1
|
|
), "model_access={}, model_access_level={}".format(
|
|
model_access, model_access_level
|
|
)
|
|
assert model_list["data"][0]["id"] == model_access
|
|
elif model_endpoint == "/model/info":
|
|
assert isinstance(model_list["data"], list)
|
|
assert len(model_list["data"]) == 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_key_user_not_in_db():
|
|
"""
|
|
- Create a key with unique user-id (not in db)
|
|
- Check if key can make `/chat/completion` call
|
|
"""
|
|
my_unique_user = str(uuid.uuid4())
|
|
async with aiohttp.ClientSession() as session:
|
|
key_gen = await generate_key(
|
|
session=session,
|
|
i=0,
|
|
user_id=my_unique_user,
|
|
)
|
|
key = key_gen["key"]
|
|
try:
|
|
await chat_completion(session=session, key=key)
|
|
except Exception as e:
|
|
pytest.fail(f"Expected this call to work - {str(e)}")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_key_over_budget():
|
|
"""
|
|
Test if key over budget is handled as expected.
|
|
"""
|
|
async with aiohttp.ClientSession() as session:
|
|
key_gen = await generate_key(session=session, i=0, budget=0.0000001)
|
|
key = key_gen["key"]
|
|
try:
|
|
await chat_completion(session=session, key=key)
|
|
except Exception as e:
|
|
pytest.fail(f"Expected this call to work - {str(e)}")
|
|
|
|
## CALL `/models` - expect to work
|
|
model_list = await get_key_info(session=session, get_key=key, call_key=key)
|
|
## CALL `/chat/completions` - expect to fail
|
|
with pytest.raises(Exception, match="Budget has been exceeded!") as exc_info:
|
|
await chat_completion(session=session, key=key)
|
|
e = exc_info.value
|
|
assert "Budget has been exceeded!" in str(e)
|