mirror of
https://github.com/tiennm99/litellm.git
synced 2026-06-18 00:48:01 +00:00
a6c30b30bf
* build: migrate packaging metadata to uv * ci: move automation and local tooling to uv * docker: migrate image builds and runtime setup to uv * docs: update install and deployment guidance for uv * chore: align auxiliary scripts and tests with uv * test: harden test_litellm isolation * fix: keep release and health check images self-contained * build: pin uv tooling and health check deps * test: isolate bedrock image request formatting from suite state * test: cover sandbox executor requirements flow * ci: fix circleci no-op command steps * ci: fix circleci publish workflow parsing * fix: stabilize remaining uv migration CI checks * ci: increase matrix test timeout headroom * fix: restore published docker and license coverage * fix: restore proxy runtime build parity * fix: restore proxy extras parity and venv migrations * ci: persist uv path across circleci steps * fix: keep psycopg binary in default test env * docker: preserve prisma cache across stages * test: run local proxy checks through uv python * build: restore runtime deps moved into ci * build: refresh uv lock after upstream merge * fix: restore module import in test_check_migration after merge The conflict resolution imported only the function but the test body references check_migration as a module throughout. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: revert dependency promotions, remove nodejs-wheel-binaries, fix Docker layer caching - Move google-generativeai, Pillow, tenacity back to ci group (they are lazily imported and bloat the base SDK install needlessly) - Remove nodejs-wheel-binaries from extra_proxy and proxy-dev (redundant in Docker where system Node.js is already installed via apk) - Remove all nodejs-wheel node replacement and venv npm patching blocks from Dockerfiles since the wheel is no longer installed - Add --no-default-groups to CodSpeed benchmark workflow so the benchmark environment matches the old minimal pip install footprint - Apply standard uv two-phase Docker pattern: copy metadata first, install deps (cached layer), then copy source and install project - Replace CircleCI enterprise no-op with proper uv sync command Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: regenerate uv.lock after removing nodejs-wheel-binaries Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(ci): use cache/restore instead of cache to prevent cache poisoning The old workflow used actions/cache/restore (read-only). The uv migration changed it to actions/cache (read-write), which zizmor flags as a cache poisoning risk. Restore the safer read-only variant. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(ci): disable setup-uv built-in cache to silence cache-poisoning alert The setup-uv action enables caching by default, which zizmor flags as a cache poisoning risk. Disable it since we already use a read-only cache/restore step. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(ci): disable setup-uv cache in publish workflow Silences zizmor cache-poisoning alert. Publishing workflow runs infrequently on protected branches so caching adds no real benefit. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(test): remove duplicate verbose_logger mock in test_check_migration The logger was patched twice — first via mocker.patch() then via mocker.patch.object(autospec=True). The second call fails because autospec cannot inspect an already-mocked attribute. Remove the redundant first patch. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(ci): free disk space before Docker build in test-server-root-path The Dockerfile.non_root build ran out of disk on the CI runner. Remove Android SDK, .NET, Boost, and GHC toolchains (~12GB) to free space. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
230 lines
6.5 KiB
Python
230 lines
6.5 KiB
Python
import importlib
|
|
import importlib.util
|
|
from importlib.machinery import PathFinder
|
|
import site
|
|
import sys
|
|
|
|
import pytest
|
|
import requests
|
|
from litellm.proxy.client.chat import ChatClient
|
|
from litellm.proxy.client.exceptions import UnauthorizedError
|
|
|
|
|
|
def _load_http_mocking_responses():
|
|
"""Load the third-party `responses` package even if test collection creates
|
|
a top-level `responses` namespace package from `tests/test_litellm/responses`.
|
|
"""
|
|
module = importlib.import_module("responses")
|
|
if hasattr(module, "activate"):
|
|
return module
|
|
|
|
for module_name in list(sys.modules):
|
|
if module_name == "responses" or module_name.startswith("responses."):
|
|
sys.modules.pop(module_name, None)
|
|
|
|
search_paths = []
|
|
try:
|
|
search_paths.extend(site.getsitepackages())
|
|
except AttributeError:
|
|
pass
|
|
user_site = site.getusersitepackages()
|
|
if isinstance(user_site, str):
|
|
search_paths.append(user_site)
|
|
else:
|
|
search_paths.extend(user_site)
|
|
|
|
spec = PathFinder.find_spec("responses", search_paths)
|
|
if spec is None or spec.loader is None:
|
|
raise ImportError("Unable to load the third-party `responses` package")
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules["responses"] = module
|
|
spec.loader.exec_module(module)
|
|
|
|
if not hasattr(module, "activate"):
|
|
raise ImportError("Unable to load the third-party `responses` package")
|
|
return module
|
|
|
|
|
|
responses = _load_http_mocking_responses()
|
|
|
|
|
|
@pytest.fixture
|
|
def base_url():
|
|
return "http://localhost:8000"
|
|
|
|
|
|
@pytest.fixture
|
|
def api_key():
|
|
return "test-api-key"
|
|
|
|
|
|
@pytest.fixture
|
|
def client(base_url, api_key):
|
|
return ChatClient(base_url=base_url, api_key=api_key)
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_messages():
|
|
return [
|
|
{"role": "system", "content": "You are a helpful assistant."},
|
|
{"role": "user", "content": "Name 3 countries"},
|
|
]
|
|
|
|
|
|
def test_client_initialization(base_url, api_key):
|
|
"""Test that the ChatClient is properly initialized"""
|
|
client = ChatClient(base_url=base_url, api_key=api_key)
|
|
|
|
assert client._base_url == base_url
|
|
assert client._api_key == api_key
|
|
|
|
|
|
def test_client_initialization_strips_trailing_slash():
|
|
"""Test that the client properly strips trailing slashes from base_url during initialization"""
|
|
base_url = "http://localhost:8000/////"
|
|
client = ChatClient(base_url=base_url)
|
|
|
|
assert client._base_url == "http://localhost:8000"
|
|
|
|
|
|
def test_client_without_api_key(base_url):
|
|
"""Test that the client works without an API key"""
|
|
client = ChatClient(base_url=base_url)
|
|
|
|
assert client._api_key is None
|
|
|
|
|
|
def test_completions_request_creation(client, base_url, api_key, sample_messages):
|
|
"""Test that completions creates a request with correct URL, headers, and body"""
|
|
request = client.completions(
|
|
model="gpt-4",
|
|
messages=sample_messages,
|
|
temperature=0.7,
|
|
max_tokens=100,
|
|
return_request=True,
|
|
)
|
|
|
|
# Check request method and URL
|
|
assert request.method == "POST"
|
|
assert request.url == f"{base_url}/chat/completions"
|
|
|
|
# Check headers
|
|
assert request.headers["Content-Type"] == "application/json"
|
|
assert request.headers["Authorization"] == f"Bearer {api_key}"
|
|
|
|
# Check request body
|
|
assert request.json == {
|
|
"model": "gpt-4",
|
|
"messages": sample_messages,
|
|
"temperature": 0.7,
|
|
"max_tokens": 100,
|
|
}
|
|
|
|
|
|
def test_completions_minimal_request(client, sample_messages):
|
|
"""Test that completions works with only required parameters"""
|
|
request = client.completions(
|
|
model="gpt-4", messages=sample_messages, return_request=True
|
|
)
|
|
|
|
# Check request body has only required fields
|
|
assert request.json == {"model": "gpt-4", "messages": sample_messages}
|
|
|
|
|
|
def test_completions_all_parameters(client, sample_messages):
|
|
"""Test that completions accepts all optional parameters"""
|
|
request = client.completions(
|
|
model="gpt-4",
|
|
messages=sample_messages,
|
|
temperature=0.7,
|
|
top_p=0.9,
|
|
n=2,
|
|
max_tokens=100,
|
|
presence_penalty=0.5,
|
|
frequency_penalty=-0.5,
|
|
user="test-user",
|
|
return_request=True,
|
|
)
|
|
|
|
# Check all parameters are included in request body
|
|
assert request.json == {
|
|
"model": "gpt-4",
|
|
"messages": sample_messages,
|
|
"temperature": 0.7,
|
|
"top_p": 0.9,
|
|
"n": 2,
|
|
"max_tokens": 100,
|
|
"presence_penalty": 0.5,
|
|
"frequency_penalty": -0.5,
|
|
"user": "test-user",
|
|
}
|
|
|
|
|
|
@responses.activate
|
|
def test_completions_mock_response(client, sample_messages):
|
|
"""Test completions with a mocked successful response"""
|
|
mock_response = {
|
|
"id": "chatcmpl-123",
|
|
"object": "chat.completion",
|
|
"created": 1677858242,
|
|
"model": "gpt-4",
|
|
"usage": {"prompt_tokens": 13, "completion_tokens": 7, "total_tokens": 20},
|
|
"choices": [
|
|
{
|
|
"message": {
|
|
"role": "assistant",
|
|
"content": "Hello! How can I help you today?",
|
|
},
|
|
"finish_reason": "stop",
|
|
"index": 0,
|
|
}
|
|
],
|
|
}
|
|
|
|
# Mock the POST request
|
|
responses.add(
|
|
responses.POST,
|
|
f"{client._base_url}/chat/completions",
|
|
json=mock_response,
|
|
status=200,
|
|
)
|
|
|
|
response = client.completions(model="gpt-4", messages=sample_messages)
|
|
|
|
assert response == mock_response
|
|
assert (
|
|
response["choices"][0]["message"]["content"]
|
|
== "Hello! How can I help you today?"
|
|
)
|
|
|
|
|
|
@responses.activate
|
|
def test_completions_unauthorized_error(client, sample_messages):
|
|
"""Test that completions raises UnauthorizedError for 401 responses"""
|
|
# Mock a 401 response
|
|
responses.add(
|
|
responses.POST,
|
|
f"{client._base_url}/chat/completions",
|
|
status=401,
|
|
json={"error": "Unauthorized"},
|
|
)
|
|
|
|
with pytest.raises(UnauthorizedError):
|
|
client.completions(model="gpt-4", messages=sample_messages)
|
|
|
|
|
|
@responses.activate
|
|
def test_completions_other_errors(client, sample_messages):
|
|
"""Test that completions raises HTTPError for other error responses"""
|
|
# Mock a 500 response
|
|
responses.add(
|
|
responses.POST,
|
|
f"{client._base_url}/chat/completions",
|
|
status=500,
|
|
json={"error": "Internal Server Error"},
|
|
)
|
|
|
|
with pytest.raises(requests.exceptions.HTTPError) as exc_info:
|
|
client.completions(model="gpt-4", messages=sample_messages)
|
|
assert exc_info.value.response.status_code == 500
|