litellm/tests/llm_translation/test_huggingface_chat_completion.py
yuneng-jiang 6a0d03914c
test: drop the cwd-relative sys.path.insert calls from the test suite (#37802)
* 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
2026-08-22 09:25:58 -07:00

590 lines
20 KiB
Python

"""
Test HuggingFace LLM
"""
import json
from unittest.mock import AsyncMock, MagicMock, patch
from base_llm_unit_tests import BaseLLMChatTest
import pytest
import litellm
from litellm.types.utils import ModelResponse, ModelResponseStream
MOCK_COMPLETION_RESPONSE = {
"id": "9115d3daeab10608",
"object": "chat.completion",
"created": 11111,
"model": "meta-llama/Meta-Llama-3-8B-Instruct",
"prompt": [],
"choices": [
{
"finish_reason": "stop",
"seed": 3629048360264764400,
"logprobs": None,
"index": 0,
"message": {
"role": "assistant",
"content": "This is a test response from the mocked HuggingFace API.",
"tool_calls": [],
},
}
],
"usage": {"prompt_tokens": 10, "completion_tokens": 20, "total_tokens": 30},
}
MOCK_STREAMING_CHUNKS = [
{
"id": "id1",
"object": "chat.completion.chunk",
"created": 1111,
"choices": [
{
"index": 0,
"text": "Deep",
"logprobs": None,
"finish_reason": None,
"seed": None,
"delta": {
"token_id": 34564,
"role": "assistant",
"content": "Deep",
"tool_calls": None,
},
}
],
"model": "meta-llama/Meta-Llama-3-8B-Instruct-Turbo",
"usage": None,
},
{
"id": "id2",
"object": "chat.completion.chunk",
"created": 1111,
"choices": [
{
"index": 0,
"text": " learning",
"logprobs": None,
"finish_reason": None,
"seed": None,
"delta": {
"token_id": 6975,
"role": "assistant",
"content": " learning",
"tool_calls": None,
},
}
],
"model": "meta-llama/Meta-Llama-3-8B-Instruct-Turbo",
"usage": None,
},
{
"id": "id3",
"object": "chat.completion.chunk",
"created": 1111,
"choices": [
{
"index": 0,
"text": " is",
"logprobs": None,
"finish_reason": None,
"seed": None,
"delta": {
"token_id": 374,
"role": "assistant",
"content": " is",
"tool_calls": None,
},
}
],
"model": "meta-llama/Meta-Llama-3-8B-Instruct-Turbo",
"usage": None,
},
{
"id": "sid4",
"object": "chat.completion.chunk",
"created": 1111,
"choices": [
{
"index": 0,
"text": " response",
"logprobs": None,
"finish_reason": "length",
"seed": 2853637492034609700,
"delta": {
"token_id": 323,
"role": "assistant",
"content": " response",
"tool_calls": None,
},
}
],
"model": "meta-llama/Meta-Llama-3-8B-Instruct-Turbo",
"usage": {"prompt_tokens": 26, "completion_tokens": 20, "total_tokens": 46},
},
]
PROVIDER_MAPPING_RESPONSE = {
"fireworks-ai": {
"status": "live",
"providerId": "accounts/fireworks/models/llama-v3-8b-instruct",
"task": "conversational",
},
"together": {
"status": "live",
"providerId": "meta-llama/Meta-Llama-3-8B-Instruct-Turbo",
"task": "conversational",
},
"hf-inference": {
"status": "live",
"providerId": "meta-llama/Meta-Llama-3-8B-Instruct",
"task": "conversational",
},
}
@pytest.fixture
def mock_provider_mapping():
with patch(
"litellm.llms.huggingface.chat.transformation._fetch_inference_provider_mapping"
) as mock:
mock.return_value = PROVIDER_MAPPING_RESPONSE
yield mock
@pytest.fixture(autouse=True)
def clear_lru_cache():
from litellm.llms.huggingface.common_utils import _fetch_inference_provider_mapping
_fetch_inference_provider_mapping.cache_clear()
yield
_fetch_inference_provider_mapping.cache_clear()
@pytest.fixture
def mock_http_handler():
"""Fixture to mock the HTTP handler"""
with patch("litellm.llms.custom_httpx.http_handler.HTTPHandler.post") as mock:
print(f"Creating mock HTTP handler: {mock}") # noqa: T201
mock_response = MagicMock()
mock_response.raise_for_status.return_value = None
mock_response.status_code = 200
def mock_side_effect(*args, **kwargs):
if kwargs.get("stream", True):
mock_response.iter_lines.return_value = iter(
[
f"data: {json.dumps(chunk)}".encode("utf-8")
for chunk in MOCK_STREAMING_CHUNKS
]
+ [b"data: [DONE]"]
)
else:
mock_response.json.return_value = MOCK_COMPLETION_RESPONSE
return mock_response
mock.side_effect = mock_side_effect
yield mock
@pytest.fixture
def mock_http_async_handler():
"""Fixture to mock the async HTTP handler"""
with patch(
"litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler.post",
new_callable=AsyncMock,
) as mock:
print(f"Creating mock async HTTP handler: {mock}") # noqa: T201
mock_response = MagicMock()
mock_response.raise_for_status.return_value = None
mock_response.status_code = 200
mock_response.headers = {"content-type": "application/json"}
mock_response.json.return_value = MOCK_COMPLETION_RESPONSE
mock_response.text = json.dumps(MOCK_COMPLETION_RESPONSE)
async def mock_side_effect(*args, **kwargs):
if kwargs.get("stream", True):
async def mock_aiter():
for chunk in MOCK_STREAMING_CHUNKS:
yield f"data: {json.dumps(chunk)}".encode("utf-8")
yield b"data: [DONE]"
mock_response.aiter_lines = mock_aiter
return mock_response
mock.side_effect = mock_side_effect
yield mock
class TestHuggingFace(BaseLLMChatTest):
@pytest.fixture(autouse=True)
def setup(self, mock_provider_mapping, mock_http_handler, mock_http_async_handler):
self.mock_provider_mapping = mock_provider_mapping
self.mock_http = mock_http_handler
self.mock_http_async = mock_http_async_handler
self.model = "huggingface/together/meta-llama/Meta-Llama-3-8B-Instruct"
litellm.set_verbose = False
def get_base_completion_call_args(self) -> dict:
"""Implementation of abstract method from BaseLLMChatTest"""
return {"model": self.model}
def test_completion_non_streaming(self):
messages = [{"role": "user", "content": "This is a dummy message"}]
response = litellm.completion(model=self.model, messages=messages, stream=False)
assert isinstance(response, ModelResponse)
assert (
response.choices[0].message.content
== "This is a test response from the mocked HuggingFace API."
)
assert response.usage is not None
assert response.model == self.model.split("/", 2)[2]
def test_completion_streaming(self):
messages = [{"role": "user", "content": "This is a dummy message"}]
response = litellm.completion(model=self.model, messages=messages, stream=True)
chunks = list(response)
assert len(chunks) > 0
assert self.mock_http.called
call_args = self.mock_http.call_args
assert call_args is not None
kwargs = call_args[1]
data = json.loads(kwargs["data"])
assert data["stream"] is True
assert data["messages"] == messages
assert isinstance(chunks, list)
assert isinstance(chunks[0], ModelResponseStream)
assert isinstance(chunks[0].id, str)
assert chunks[0].model == self.model.split("/", 1)[1]
@pytest.mark.asyncio
async def test_async_completion_streaming(self):
"""Test async streaming completion"""
messages = [{"role": "user", "content": "This is a dummy message"}]
response = await litellm.acompletion(
model=self.model, messages=messages, stream=True
)
chunks = []
async for chunk in response:
chunks.append(chunk)
assert self.mock_http_async.called
assert len(chunks) > 0
assert isinstance(chunks[0], ModelResponseStream)
assert isinstance(chunks[0].id, str)
assert chunks[0].model == self.model.split("/", 1)[1]
@pytest.mark.asyncio
async def test_async_completion_non_streaming(self):
"""Test async non-streaming completion"""
messages = [{"role": "user", "content": "This is a dummy message"}]
response = await litellm.acompletion(
model=self.model, messages=messages, stream=False
)
assert self.mock_http_async.called
assert isinstance(response, ModelResponse)
assert (
response.choices[0].message.content
== "This is a test response from the mocked HuggingFace API."
)
assert response.usage is not None
assert response.model == self.model.split("/", 2)[2]
def test_tool_call_no_arguments(self, tool_call_no_arguments):
mock_tool_response = {
**MOCK_COMPLETION_RESPONSE,
"choices": [
{
"finish_reason": "tool_calls",
"index": 0,
"message": tool_call_no_arguments,
}
],
}
with patch.object(
self.mock_http,
"side_effect",
lambda *args, **kwargs: MagicMock(
status_code=200,
json=lambda: mock_tool_response,
raise_for_status=lambda: None,
),
):
messages = [{"role": "user", "content": "Get the FAQ"}]
tools = [
{
"type": "function",
"function": {
"name": "Get-FAQ",
"description": "Get FAQ information",
"parameters": {
"type": "object",
"properties": {},
"required": [],
},
},
}
]
response = litellm.completion(
model=self.model, messages=messages, tools=tools, tool_choice="auto"
)
assert response.choices[0].message.tool_calls is not None
assert len(response.choices[0].message.tool_calls) == 1
assert (
response.choices[0].message.tool_calls[0].function.name
== tool_call_no_arguments["tool_calls"][0]["function"]["name"]
)
assert (
response.choices[0].message.tool_calls[0].function.arguments
== tool_call_no_arguments["tool_calls"][0]["function"]["arguments"]
)
@pytest.mark.parametrize(
"model, expected_url",
[
(
"meta-llama/Llama-3-8B-Instruct",
"https://router.huggingface.co/v1/chat/completions",
),
(
"together/meta-llama/Llama-3-8B-Instruct",
"https://router.huggingface.co/together/v1/chat/completions",
),
(
"novita/meta-llama/Llama-3-8B-Instruct",
"https://router.huggingface.co/novita/v3/openai/chat/completions",
),
(
"http://custom-endpoint.com/v1/chat/completions",
"http://custom-endpoint.com/v1/chat/completions",
),
],
)
def test_get_complete_url(self, model, expected_url):
"""Test that the complete URL is constructed correctly for different providers"""
from litellm.llms.huggingface.chat.transformation import HuggingFaceChatConfig
config = HuggingFaceChatConfig()
url = config.get_complete_url(
api_base=None,
model=model,
optional_params={},
stream=False,
api_key="test_api_key",
litellm_params={},
)
assert url == expected_url
@pytest.mark.parametrize(
"api_base, model, expected_url",
[
(
"https://abcd123.us-east-1.aws.endpoints.huggingface.cloud",
"huggingface/tgi",
"https://abcd123.us-east-1.aws.endpoints.huggingface.cloud/v1/chat/completions",
),
(
"https://abcd123.us-east-1.aws.endpoints.huggingface.cloud/",
"huggingface/tgi",
"https://abcd123.us-east-1.aws.endpoints.huggingface.cloud/v1/chat/completions",
),
(
"https://abcd123.us-east-1.aws.endpoints.huggingface.cloud/v1/chat/completions",
"huggingface/tgi",
"https://abcd123.us-east-1.aws.endpoints.huggingface.cloud/v1/chat/completions",
),
(
"https://example.com/custom/path",
"huggingface/tgi",
"https://example.com/custom/path/v1/chat/completions",
),
(
"https://example.com/custom/path/v1/chat/completions",
"huggingface/tgi",
"https://example.com/custom/path/v1/chat/completions",
),
(
"https://example.com/v1",
"huggingface/tgi",
"https://example.com/v1/chat/completions",
),
],
)
def test_get_complete_url_inference_endpoints(self, api_base, model, expected_url):
from litellm.llms.huggingface.chat.transformation import HuggingFaceChatConfig
config = HuggingFaceChatConfig()
url = config.get_complete_url(
api_base=api_base,
model=model,
optional_params={},
stream=False,
api_key="test_api_key",
litellm_params={},
)
assert url == expected_url
def test_completion_with_api_base(self):
messages = [{"role": "user", "content": "This is a test message"}]
api_base = "https://abcd123.us-east-1.aws.endpoints.huggingface.cloud"
response = litellm.completion(
model="huggingface/tgi", messages=messages, api_base=api_base, stream=False
)
assert isinstance(response, ModelResponse)
assert (
response.choices[0].message.content
== "This is a test response from the mocked HuggingFace API."
)
assert self.mock_http.called
call_args = self.mock_http.call_args
assert call_args is not None
called_url = call_args[1]["url"]
assert called_url == f"{api_base}/v1/chat/completions"
@pytest.mark.asyncio
async def test_async_completion_with_api_base(self):
messages = [{"role": "user", "content": "This is a test message"}]
api_base = "https://abcd123.us-east-1.aws.endpoints.huggingface.cloud"
response = await litellm.acompletion(
model="huggingface/tgi", messages=messages, api_base=api_base, stream=False
)
assert isinstance(response, ModelResponse)
assert (
response.choices[0].message.content
== "This is a test response from the mocked HuggingFace API."
)
assert self.mock_http_async.called
call_args = self.mock_http_async.call_args
assert call_args is not None
called_url = call_args[1]["url"]
assert called_url == f"{api_base}/v1/chat/completions"
def test_completion_streaming_with_api_base(self):
"""Test streaming completion with api_base parameter"""
messages = [{"role": "user", "content": "This is a test message"}]
api_base = "https://abcd123.us-east-1.aws.endpoints.huggingface.cloud"
response = litellm.completion(
model="huggingface/tgi", messages=messages, api_base=api_base, stream=True
)
chunks = list(response)
assert len(chunks) > 0
assert isinstance(chunks[0], ModelResponseStream)
# Check that the correct URL was called
assert self.mock_http.called
call_args = self.mock_http.call_args
assert call_args is not None
called_url = call_args[1]["url"]
assert called_url == f"{api_base}/v1/chat/completions"
def test_build_chat_completion_url_function(self):
"""Test the _build_chat_completion_url helper function"""
from litellm.llms.huggingface.chat.transformation import (
_build_chat_completion_url,
)
test_cases = [
("https://example.com", "https://example.com/v1/chat/completions"),
("https://example.com/", "https://example.com/v1/chat/completions"),
("https://example.com/v1", "https://example.com/v1/chat/completions"),
("https://example.com/v1/", "https://example.com/v1/chat/completions"),
(
"https://example.com/v1/chat/completions",
"https://example.com/v1/chat/completions",
),
(
"https://example.com/custom/path",
"https://example.com/custom/path/v1/chat/completions",
),
(
"https://example.com/custom/path/",
"https://example.com/custom/path/v1/chat/completions",
),
]
for input_url, expected_url in test_cases:
result = _build_chat_completion_url(input_url)
assert (
result == expected_url
), f"Failed for input: {input_url}, expected: {expected_url}, got: {result}"
def test_validate_environment(self):
"""Test that the environment is validated correctly"""
from litellm.llms.huggingface.chat.transformation import HuggingFaceChatConfig
config = HuggingFaceChatConfig()
headers = config.validate_environment(
headers={},
model="huggingface/fireworks-ai/meta-llama/Meta-Llama-3-8B-Instruct",
messages=[{"role": "user", "content": "Hello"}],
optional_params={},
api_key="test_api_key",
litellm_params={},
)
assert headers["Authorization"] == "Bearer test_api_key"
assert headers["content-type"] == "application/json"
@pytest.mark.parametrize(
"model, expected_model",
[
(
"together/meta-llama/Llama-3-8B-Instruct",
"meta-llama/Meta-Llama-3-8B-Instruct-Turbo",
),
(
"meta-llama/Meta-Llama-3-8B-Instruct",
"meta-llama/Meta-Llama-3-8B-Instruct",
),
],
)
def test_transform_request(self, model, expected_model):
from litellm.llms.huggingface.chat.transformation import HuggingFaceChatConfig
config = HuggingFaceChatConfig()
messages = [{"role": "user", "content": "Hello"}]
transformed_request = config.transform_request(
model=model,
messages=messages,
optional_params={},
litellm_params={},
headers={},
)
assert transformed_request["model"] == expected_model
assert transformed_request["messages"] == messages
@pytest.mark.asyncio
async def test_completion_cost(self):
pass