Merge pull request #42112 from BerriAI/litellm_migrate_tests_p8

test: migrate wave 1 phase 8 legacy llm tests to tests/unit
This commit is contained in:
yuneng-jiang 2026-09-20 03:59:36 -07:00 • committed by GitHub
commit 766f6fcd3a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 84 additions and 616 deletions

View file

@ -1,147 +0,0 @@
"""
Test SSL verification for hosted_vllm provider.
This test ensures that the ssl_verify parameter is properly passed through
to the HTTP client when using the hosted_vllm provider.
Issue: ssl_verify parameter was being ignored because hosted_vllm fell through
to the OpenAI catch-all path in main.py, which doesn't pass ssl_verify to the HTTP client.
"""
from unittest.mock import MagicMock, patch
import pytest
import litellm
class TestHostedVLLMSSLVerify:
"""Test suite for SSL verification in hosted_vllm provider."""
@patch("litellm.llms.custom_httpx.llm_http_handler._get_httpx_client")
def test_hosted_vllm_ssl_verify_false_sync(self, mock_get_httpx_client):
"""Test that ssl_verify=False is passed to the HTTP client for sync calls."""
# Setup mock client
mock_client = MagicMock()
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.headers = {"content-type": "application/json"}
mock_response.json.return_value = {
"id": "chatcmpl-test",
"object": "chat.completion",
"created": 1234567890,
"model": "test-model",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Test response",
},
"finish_reason": "stop",
}
],
"usage": {
"prompt_tokens": 10,
"completion_tokens": 5,
"total_tokens": 15,
},
}
mock_response.text = '{"id": "chatcmpl-test", "object": "chat.completion", "created": 1234567890, "model": "test-model", "choices": [{"index": 0, "message": {"role": "assistant", "content": "Test response"}, "finish_reason": "stop"}], "usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}}'
mock_client.post.return_value = mock_response
mock_get_httpx_client.return_value = mock_client
try:
litellm.completion(
model="hosted_vllm/test-model",
messages=[{"role": "user", "content": "Hello"}],
api_base="https://test-vllm.example.com/v1",
ssl_verify=False,
)
except Exception:
# Even if the response parsing fails, we just need to verify
# that the mock was called with the correct ssl_verify parameter
pass
# Verify _get_httpx_client was called with ssl_verify=False
mock_get_httpx_client.assert_called()
call_args = mock_get_httpx_client.call_args
# Check that params contains ssl_verify=False
if call_args[0]:
# Positional argument
params = call_args[0][0]
else:
# Keyword argument
params = call_args[1].get("params", {})
assert (
params.get("ssl_verify") is False
), f"Expected ssl_verify=False in params, got {params}"
@patch("litellm.llms.custom_httpx.llm_http_handler.get_async_httpx_client")
@pytest.mark.asyncio
async def test_hosted_vllm_ssl_verify_false_async(
self, mock_get_async_httpx_client
):
"""Test that ssl_verify=False is passed to the HTTP client for async calls."""
# Setup mock async client
mock_client = MagicMock()
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.headers = {"content-type": "application/json"}
mock_response.json.return_value = {
"id": "chatcmpl-test",
"object": "chat.completion",
"created": 1234567890,
"model": "test-model",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Test response",
},
"finish_reason": "stop",
}
],
"usage": {
"prompt_tokens": 10,
"completion_tokens": 5,
"total_tokens": 15,
},
}
mock_response.text = '{"id": "chatcmpl-test", "object": "chat.completion", "created": 1234567890, "model": "test-model", "choices": [{"index": 0, "message": {"role": "assistant", "content": "Test response"}, "finish_reason": "stop"}], "usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}}'
async def mock_post(*args, **kwargs):
return mock_response
mock_client.post = mock_post
mock_get_async_httpx_client.return_value = mock_client
try:
await litellm.acompletion(
model="hosted_vllm/test-model",
messages=[{"role": "user", "content": "Hello"}],
api_base="https://test-vllm.example.com/v1",
ssl_verify=False,
)
except Exception:
# Even if the response parsing fails, we just need to verify
# that the mock was called with the correct ssl_verify parameter
pass
# Verify get_async_httpx_client was called with ssl_verify=False
mock_get_async_httpx_client.assert_called()
call_kwargs = mock_get_async_httpx_client.call_args[1]
# Check that params contains ssl_verify=False
params = call_kwargs.get("params", {})
assert (
params.get("ssl_verify") is False
), f"Expected ssl_verify=False in params, got {params}"
if __name__ == "__main__":
pytest.main([__file__, "-v", "-s"])

View file

@ -1,135 +0,0 @@
"""
Test SSL verification for hosted_vllm provider embeddings.
This test ensures that the ssl_verify parameter is properly passed through
to the HTTP client when using the hosted_vllm provider for embeddings.
Issue: ssl_verify parameter was being ignored because hosted_vllm fell through
to the openai_like catch-all path in main.py, which doesn't pass ssl_verify to the HTTP client.
"""
from unittest.mock import MagicMock, patch
import pytest
import litellm
class TestHostedVLLMEmbeddingSSLVerify:
"""Test suite for SSL verification in hosted_vllm provider embeddings."""
@patch("litellm.llms.custom_httpx.llm_http_handler._get_httpx_client")
def test_hosted_vllm_embedding_ssl_verify_false_sync(self, mock_get_httpx_client):
"""Test that ssl_verify=False is passed to the HTTP client for sync embedding calls."""
# Setup mock client
mock_client = MagicMock()
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.headers = {"content-type": "application/json"}
mock_response.json.return_value = {
"object": "list",
"data": [
{
"object": "embedding",
"index": 0,
"embedding": [0.1, 0.2, 0.3, 0.4, 0.5],
}
],
"model": "text-embedding-model",
"usage": {
"prompt_tokens": 5,
"total_tokens": 5,
},
}
mock_response.text = '{"object": "list", "data": [{"object": "embedding", "index": 0, "embedding": [0.1, 0.2, 0.3, 0.4, 0.5]}], "model": "text-embedding-model", "usage": {"prompt_tokens": 5, "total_tokens": 5}}'
mock_client.post.return_value = mock_response
mock_get_httpx_client.return_value = mock_client
try:
litellm.embedding(
model="hosted_vllm/text-embedding-model",
input=["hello world"],
api_base="https://test-vllm.example.com/v1",
ssl_verify=False,
)
except Exception:
# Even if the response parsing fails, we just need to verify
# that the mock was called with the correct ssl_verify parameter
pass
# Verify _get_httpx_client was called with ssl_verify=False
mock_get_httpx_client.assert_called()
call_args = mock_get_httpx_client.call_args
# Check that params contains ssl_verify=False
if call_args[0]:
# Positional argument
params = call_args[0][0]
else:
# Keyword argument
params = call_args[1].get("params", {})
assert (
params.get("ssl_verify") is False
), f"Expected ssl_verify=False in params, got {params}"
@patch("litellm.llms.custom_httpx.llm_http_handler.get_async_httpx_client")
@pytest.mark.asyncio
async def test_hosted_vllm_embedding_ssl_verify_false_async(
self, mock_get_async_httpx_client
):
"""Test that ssl_verify=False is passed to the HTTP client for async embedding calls."""
# Setup mock async client
mock_client = MagicMock()
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.headers = {"content-type": "application/json"}
mock_response.json.return_value = {
"object": "list",
"data": [
{
"object": "embedding",
"index": 0,
"embedding": [0.1, 0.2, 0.3, 0.4, 0.5],
}
],
"model": "text-embedding-model",
"usage": {
"prompt_tokens": 5,
"total_tokens": 5,
},
}
mock_response.text = '{"object": "list", "data": [{"object": "embedding", "index": 0, "embedding": [0.1, 0.2, 0.3, 0.4, 0.5]}], "model": "text-embedding-model", "usage": {"prompt_tokens": 5, "total_tokens": 5}}'
async def mock_post(*args, **kwargs):
return mock_response
mock_client.post = mock_post
mock_get_async_httpx_client.return_value = mock_client
try:
await litellm.aembedding(
model="hosted_vllm/text-embedding-model",
input=["hello world"],
api_base="https://test-vllm.example.com/v1",
ssl_verify=False,
)
except Exception:
# Even if the response parsing fails, we just need to verify
# that the mock was called with the correct ssl_verify parameter
pass
# Verify get_async_httpx_client was called with ssl_verify=False
mock_get_async_httpx_client.assert_called()
call_kwargs = mock_get_async_httpx_client.call_args[1]
# Check that params contains ssl_verify=False
params = call_kwargs.get("params", {})
assert (
params.get("ssl_verify") is False
), f"Expected ssl_verify=False in params, got {params}"
if __name__ == "__main__":
pytest.main([__file__, "-v", "-s"])

View file

@ -10,13 +10,6 @@ from litellm.llms.github_copilot.messages.transformation import (
)
def test_github_copilot_anthropic_messages_config_init():
"""Test GithubCopilotAnthropicMessagesConfig initialization."""
config = GithubCopilotAnthropicMessagesConfig()
assert config is not None
assert hasattr(config, "authenticator")
def test_github_copilot_anthropic_messages_get_complete_url():
"""get_complete_url builds the /v1/messages URL from the base it is handed.

View file

@ -26,9 +26,7 @@ def use_local_model_cost_map(monkeypatch: pytest.MonkeyPatch):
"""Pin litellm.model_cost to the bundled local backup so tests don't depend
on remote catalog fetches (and don't change behavior across remote refreshes)."""
monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True")
monkeypatch.setattr(
litellm, "model_cost", get_model_cost_map(url=litellm.model_cost_map_url)
)
monkeypatch.setattr(litellm, "model_cost", get_model_cost_map(url=litellm.model_cost_map_url))
litellm.add_known_models(model_cost_map=litellm.model_cost)
@ -44,49 +42,35 @@ class TestGithubCopilotResponsesAPITransformation:
provider=LlmProviders.GITHUB_COPILOT,
)
assert (
config is not None
), "Config should not be None for GitHub Copilot provider"
assert isinstance(
config, GithubCopilotResponsesAPIConfig
), f"Expected GithubCopilotResponsesAPIConfig, got {type(config)}"
assert (
config.custom_llm_provider == LlmProviders.GITHUB_COPILOT
), "custom_llm_provider should be GITHUB_COPILOT"
assert config is not None, "Config should not be None for GitHub Copilot provider"
assert isinstance(config, GithubCopilotResponsesAPIConfig), (
f"Expected GithubCopilotResponsesAPIConfig, got {type(config)}"
)
assert config.custom_llm_provider == LlmProviders.GITHUB_COPILOT, "custom_llm_provider should be GITHUB_COPILOT"
@patch("litellm.llms.github_copilot.responses.transformation.Authenticator")
def test_github_copilot_responses_endpoint_url(self, mock_authenticator_class):
"""Test that get_complete_url returns correct GitHub Copilot endpoint"""
# Mock authenticator to return default base
mock_auth_instance = MagicMock()
mock_auth_instance.get_api_base.return_value = (
"https://api.individual.githubcopilot.com"
)
mock_auth_instance.get_api_base.return_value = "https://api.individual.githubcopilot.com"
mock_authenticator_class.return_value = mock_auth_instance
config = GithubCopilotResponsesAPIConfig()
# Test with default GitHub Copilot API base (from authenticator)
url = config.get_complete_url(api_base=None, litellm_params={})
assert (
url == "https://api.individual.githubcopilot.com/responses"
), f"Expected GitHub Copilot responses endpoint, got {url}"
assert url == "https://api.individual.githubcopilot.com/responses", (
f"Expected GitHub Copilot responses endpoint, got {url}"
)
# Test with custom api_base (overrides authenticator)
custom_url = config.get_complete_url(
api_base="https://custom.githubcopilot.com", litellm_params={}
)
assert (
custom_url == "https://custom.githubcopilot.com/responses"
), f"Expected custom endpoint, got {custom_url}"
custom_url = config.get_complete_url(api_base="https://custom.githubcopilot.com", litellm_params={})
assert custom_url == "https://custom.githubcopilot.com/responses", f"Expected custom endpoint, got {custom_url}"
# Test with trailing slash
url_with_slash = config.get_complete_url(
api_base="https://api.githubcopilot.com/", litellm_params={}
)
assert (
url_with_slash == "https://api.githubcopilot.com/responses"
), "Should handle trailing slash"
url_with_slash = config.get_complete_url(api_base="https://api.githubcopilot.com/", litellm_params={})
assert url_with_slash == "https://api.githubcopilot.com/responses", "Should handle trailing slash"
@patch("litellm.llms.github_copilot.responses.transformation.Authenticator")
def test_validate_environment_default_headers(self, mock_authenticator_class):
@ -98,9 +82,7 @@ class TestGithubCopilotResponsesAPITransformation:
config = GithubCopilotResponsesAPIConfig()
headers = config.validate_environment(
headers={}, model="gpt-5.1-codex", litellm_params={}
)
headers = config.validate_environment(headers={}, model="gpt-5.1-codex", litellm_params={})
# Check required headers
assert headers["Authorization"] == "Bearer test-api-key-123"
@ -127,9 +109,7 @@ class TestGithubCopilotResponsesAPITransformation:
"custom-header": "custom-value",
}
headers = config.validate_environment(
headers=custom_headers, model="gpt-5.1-codex", litellm_params={}
)
headers = config.validate_environment(headers=custom_headers, model="gpt-5.1-codex", litellm_params={})
# User header should override default
assert headers["editor-version"] == "custom/2.0.0"
@ -182,9 +162,7 @@ class TestGithubCopilotResponsesAPITransformation:
"""Test _has_vision_input detects input_image type"""
config = GithubCopilotResponsesAPIConfig()
input_with_vision = [
{"role": "user", "content": [{"type": "input_image", "data": "base64..."}]}
]
input_with_vision = [{"role": "user", "content": [{"type": "input_image", "data": "base64..."}]}]
has_vision = config._has_vision_input(input_with_vision)
assert has_vision is True, "Should detect input_image type"
@ -246,13 +224,11 @@ class TestGithubCopilotResponsesAPITransformation:
}
]
headers = config.validate_environment(
headers={}, model="gpt-5.1-codex", litellm_params=mock_litellm_params
)
headers = config.validate_environment(headers={}, model="gpt-5.1-codex", litellm_params=mock_litellm_params)
assert (
headers.get("copilot-vision-request") == "true"
), "Should add copilot-vision-request header for vision input"
assert headers.get("copilot-vision-request") == "true", (
"Should add copilot-vision-request header for vision input"
)
@patch("litellm.llms.github_copilot.responses.transformation.Authenticator")
def test_validate_environment_with_x_initiator(self, mock_authenticator_class):
@ -270,21 +246,15 @@ class TestGithubCopilotResponsesAPITransformation:
{"role": "assistant", "content": "Hi"},
]
headers = config.validate_environment(
headers={}, model="gpt-5.1-codex", litellm_params=mock_litellm_params
)
headers = config.validate_environment(headers={}, model="gpt-5.1-codex", litellm_params=mock_litellm_params)
assert (
headers.get("X-Initiator") == "agent"
), "Should set X-Initiator to 'agent' for assistant role"
assert headers.get("X-Initiator") == "agent", "Should set X-Initiator to 'agent' for assistant role"
def test_map_openai_params_no_transformation(self):
"""Test that map_openai_params passes through parameters unchanged"""
config = GithubCopilotResponsesAPIConfig()
params = ResponsesAPIOptionalRequestParams(
temperature=0.7, max_output_tokens=1000, stream=False
)
params = ResponsesAPIOptionalRequestParams(temperature=0.7, max_output_tokens=1000, stream=False)
result = config.map_openai_params(
response_api_optional_params=params,
@ -338,9 +308,9 @@ class TestGithubCopilotResponsesAPITransformation:
result = config._handle_reasoning_item(reasoning_item)
# encrypted_content should be preserved
assert (
result.get("encrypted_content") == "encrypted-blob-abc123"
), "encrypted_content must be preserved for GitHub Copilot multi-turn conversations"
assert result.get("encrypted_content") == "encrypted-blob-abc123", (
"encrypted_content must be preserved for GitHub Copilot multi-turn conversations"
)
# status=None should be filtered out
assert "status" not in result, "status=None should be filtered out"
# content=None should be filtered out
@ -393,9 +363,7 @@ class TestGithubCopilotResponsesAPIRouting:
in the (already-merged) model info; otherwise returns None so the dispatcher
routes through the chat-completions translation bridge."""
@patch(
"litellm.llms.github_copilot.responses.transformation._cached_get_model_info_helper"
)
@patch("litellm.llms.github_copilot.responses.transformation._cached_get_model_info_helper")
def test_returns_config_when_mode_is_responses(self, mock_get_info):
"""``mode=responses`` returns native config."""
mock_get_info.return_value = {"mode": "responses"}
@ -405,9 +373,7 @@ class TestGithubCopilotResponsesAPIRouting:
)
assert isinstance(config, GithubCopilotResponsesAPIConfig)
@patch(
"litellm.llms.github_copilot.responses.transformation._cached_get_model_info_helper"
)
@patch("litellm.llms.github_copilot.responses.transformation._cached_get_model_info_helper")
def test_returns_none_when_mode_is_chat(self, mock_get_info):
"""``mode=chat`` returns None so dispatcher uses bridge."""
mock_get_info.return_value = {"mode": "chat"}
@ -417,9 +383,7 @@ class TestGithubCopilotResponsesAPIRouting:
)
assert config is None
@patch(
"litellm.llms.github_copilot.responses.transformation._cached_get_model_info_helper"
)
@patch("litellm.llms.github_copilot.responses.transformation._cached_get_model_info_helper")
def test_returns_none_when_mode_is_unset_and_no_endpoints(self, mock_get_info):
"""Entry without ``mode`` and without ``supported_endpoints`` returns None
(conservative default)."""
@ -499,9 +463,7 @@ class TestGithubCopilotResponsesAPIRouting:
)
assert isinstance(config, GithubCopilotResponsesAPIConfig)
@patch(
"litellm.llms.github_copilot.responses.transformation._cached_get_model_info_helper"
)
@patch("litellm.llms.github_copilot.responses.transformation._cached_get_model_info_helper")
def test_returns_none_when_get_model_info_raises(self, mock_get_info):
"""Catalog lookup failure (model not registered) returns None
(conservative default; bridge handles unknown models safely)."""
@ -512,9 +474,7 @@ class TestGithubCopilotResponsesAPIRouting:
)
assert config is None
@patch(
"litellm.llms.github_copilot.responses.transformation._cached_get_model_info_helper"
)
@patch("litellm.llms.github_copilot.responses.transformation._cached_get_model_info_helper")
def test_user_override_via_register_model(self, mock_get_info):
"""User-supplied per-deployment ``model_info`` flows through
``litellm.register_model`` (called by the router) into the merged
@ -528,9 +488,7 @@ class TestGithubCopilotResponsesAPIRouting:
)
assert isinstance(config, GithubCopilotResponsesAPIConfig)
@patch(
"litellm.llms.github_copilot.responses.transformation._cached_get_model_info_helper"
)
@patch("litellm.llms.github_copilot.responses.transformation._cached_get_model_info_helper")
def test_realistic_chat_only_entry_returns_none(self, mock_get_info):
"""Realistic ``model_prices_and_context_window.json`` shape for a
chat-only Copilot model (e.g. github_copilot/gemini-3.1-pro-preview)
@ -554,9 +512,7 @@ class TestGithubCopilotResponsesAPIRouting:
)
assert config is None
@patch(
"litellm.llms.github_copilot.responses.transformation._cached_get_model_info_helper"
)
@patch("litellm.llms.github_copilot.responses.transformation._cached_get_model_info_helper")
def test_realistic_responses_only_entry_returns_config(self, mock_get_info):
"""Realistic catalog entry for a Responses-only Copilot model
(e.g. github_copilot/gpt-5.5) returns the native config."""
@ -592,9 +548,7 @@ class TestGithubCopilotReasoningStreamItemIdNormalization:
output_index group to the id from its output_item.added."""
def _config(self):
with patch(
"litellm.llms.github_copilot.responses.transformation.Authenticator"
):
with patch("litellm.llms.github_copilot.responses.transformation.Authenticator"):
return GithubCopilotResponsesAPIConfig()
def _transform(self, config, chunk):

View file

@ -202,5 +202,3 @@ class TestGroqWebSearchUsageSignal:
model_response = litellm.ModelResponse()
GroqChatConfig()._add_web_search_usage(model_response=model_response)
assert getattr(model_response, "usage", None) is None

View file

@ -41,74 +41,9 @@ def test_hosted_vllm_chat_transformation_file_url():
]
def test_hosted_vllm_chat_transformation_with_audio_url():
from litellm import completion
mock_client = MagicMock()
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.headers = {"content-type": "application/json"}
mock_response.json.return_value = {
"id": "chatcmpl-test",
"object": "chat.completion",
"created": 1234567890,
"model": "llama-3.1-70b-instruct",
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": "Test response"},
"finish_reason": "stop",
}
],
"usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15},
}
mock_response.text = json.dumps(mock_response.json.return_value)
mock_client.post.return_value = mock_response
with patch(
"litellm.llms.custom_httpx.llm_http_handler._get_httpx_client",
return_value=mock_client,
):
try:
completion(
model="hosted_vllm/llama-3.1-70b-instruct",
messages=[
{
"role": "user",
"content": [
{
"type": "audio_url",
"audio_url": {"url": "https://example.com/audio.mp3"},
},
],
},
],
api_base="https://test-vllm.example.com/v1",
)
except Exception:
pass
mock_client.post.assert_called_once()
call_kwargs = mock_client.post.call_args[1]
request_data = json.loads(call_kwargs["data"])
assert request_data["messages"] == [
{
"role": "user",
"content": [
{
"type": "audio_url",
"audio_url": {"url": "https://example.com/audio.mp3"},
}
],
}
]
def test_hosted_vllm_supports_reasoning_effort():
config = HostedVLLMChatConfig()
supported_params = config.get_supported_openai_params(
model="hosted_vllm/gpt-oss-120b"
)
supported_params = config.get_supported_openai_params(model="hosted_vllm/gpt-oss-120b")
assert "reasoning_effort" in supported_params
optional_params = config.map_openai_params(
non_default_params={"reasoning_effort": "high"},
@ -129,9 +64,7 @@ def test_hosted_vllm_supports_thinking():
Related issue: https://github.com/BerriAI/litellm/issues/19761
"""
config = HostedVLLMChatConfig()
supported_params = config.get_supported_openai_params(
model="hosted_vllm/GLM-4.6-FP8"
)
supported_params = config.get_supported_openai_params(model="hosted_vllm/GLM-4.6-FP8")
assert "thinking" in supported_params
# Test thinking below the low threshold -> "minimal"

View file

@ -87,9 +87,7 @@ class TestHostedVLLMEmbeddingTransformation:
headers={},
)
assert (
"encoding_format" not in result
), "encoding_format should not be in request when not provided"
assert "encoding_format" not in result, "encoding_format should not be in request when not provided"
def test_encoding_format_not_included_when_none(self):
"""
@ -278,9 +276,7 @@ class TestHostedVLLMEmbeddingTransformation:
sent_data = json.loads(call_kwargs["data"])
# Assert that encoding_format is NOT in the sent data
assert (
"encoding_format" not in sent_data
), "encoding_format should not be in request when not provided"
assert "encoding_format" not in sent_data, "encoding_format should not be in request when not provided"
assert sent_data["model"] == "BAAI/bge-small-en-v1.5"
assert sent_data["input"] == ["Hello world"]

View file

@ -68,9 +68,7 @@ def test_hosted_vllm_responses_create_with_string_input():
Test that hosted_vllm routes directly to the native /v1/responses endpoint
when the Responses API config is registered, and correctly parses the response.
"""
mock_client = _make_mock_http_client(
_make_mock_responses_api_response("I'm doing well, thanks!")
)
mock_client = _make_mock_http_client(_make_mock_responses_api_response("I'm doing well, thanks!"))
with patch(
"litellm.llms.custom_httpx.llm_http_handler._get_httpx_client",
@ -109,10 +107,7 @@ def test_hosted_vllm_responses_create_with_explicit_none_extra_body():
)
# extra_body=None should be normalized to an empty dict (or absent)
assert (
optional_params.get("extra_body") is not None
or "extra_body" not in optional_params
)
assert optional_params.get("extra_body") is not None or "extra_body" not in optional_params
def test_hosted_vllm_provider_config_registration():

View file

@ -219,29 +219,6 @@ def test_huggingface_rerank_return_documents(mock_post):
assert "text" in result["document"]
@patch("litellm.llms.custom_httpx.http_handler.HTTPHandler.post")
def test_huggingface_rerank_error_handling(mock_post):
"""Test HuggingFace rerank error handling."""
def return_val():
return {"error": "Unauthorized"}
mock_response = MagicMock()
mock_response.status_code = 401
mock_response.json = return_val
mock_response.text = "Unauthorized"
mock_post.return_value = mock_response
with pytest.raises(litellm.APIConnectionError):
litellm.rerank(
model="huggingface/BAAI/bge-reranker-base",
query="hello",
documents=["hello", "world"],
top_n=2,
api_key="invalid_key",
)
def test_huggingface_rerank_config():
"""Test HuggingFaceRerankConfig class functionality."""
from litellm.llms.huggingface.rerank.transformation import HuggingFaceRerankConfig
@ -249,10 +226,7 @@ def test_huggingface_rerank_config():
config = HuggingFaceRerankConfig()
# Test complete URL generation
assert (
config.get_complete_url(None, "test")
== "https://api-inference.huggingface.co/rerank"
)
assert config.get_complete_url(None, "test") == "https://api-inference.huggingface.co/rerank"
# Test custom API base
custom_url = config.get_complete_url("https://custom.huggingface.co", "test")
@ -292,13 +266,9 @@ def test_request_transformation():
config = HuggingFaceRerankConfig()
optional_params = OptionalRerankParams(
query="hello", texts=["hello", "world"], top_n=2, return_text=True
)
optional_params = OptionalRerankParams(query="hello", texts=["hello", "world"], top_n=2, return_text=True)
request_body = config.transform_rerank_request(
model="test", optional_rerank_params=optional_params, headers={}
)
request_body = config.transform_rerank_request(model="test", optional_rerank_params=optional_params, headers={})
assert request_body["query"] == "hello"
assert request_body["texts"] == ["hello", "world"]
@ -368,9 +338,7 @@ def test_validate_environment():
# Test headers override
custom_headers = {"custom": "header"}
headers = config.validate_environment(
headers=custom_headers, model="test", api_key="test_key"
)
headers = config.validate_environment(headers=custom_headers, model="test", api_key="test_key")
assert "custom" in headers
assert headers["custom"] == "header"

View file

@ -188,21 +188,15 @@ def test_inception_does_not_leak_key_to_caller_api_base():
caller also supplies their own key.
"""
config = InceptionChatConfig()
with mock.patch.dict(
os.environ, {"INCEPTION_API_KEY": "server-secret"}, clear=True
):
with mock.patch.dict(os.environ, {"INCEPTION_API_KEY": "server-secret"}, clear=True):
with mock.patch.object(litellm, "inception_key", "module-secret"):
# caller overrides api_base without a key -> server key withheld
api_base, api_key = config._get_openai_compatible_provider_info(
"https://attacker.example/v1", None
)
api_base, api_key = config._get_openai_compatible_provider_info("https://attacker.example/v1", None)
assert api_base == "https://attacker.example/v1"
assert api_key is None
# caller overrides api_base AND supplies their own key -> used as-is
_, api_key = config._get_openai_compatible_provider_info(
"https://attacker.example/v1", "caller-key"
)
_, api_key = config._get_openai_compatible_provider_info("https://attacker.example/v1", "caller-key")
assert api_key == "caller-key"
# default/server base -> server-managed key resolved
@ -217,9 +211,7 @@ def test_get_llm_provider_inception():
assert model == "mercury-2"
assert provider == "inception"
model, provider, _, api_base = get_llm_provider(
"mercury-2", api_base="https://api.inceptionlabs.ai/v1"
)
model, provider, _, api_base = get_llm_provider("mercury-2", api_base="https://api.inceptionlabs.ai/v1")
assert model == "mercury-2"
assert provider == "inception"
assert api_base == "https://api.inceptionlabs.ai/v1"
@ -293,5 +285,3 @@ def test_inception_completion_targets_inception_endpoint():
assert captured["body"]["model"] == "mercury-2"
assert captured["body"]["tool_choice"] == "auto"
assert response.choices[0].message.content == "hi"

View file

@ -22,9 +22,7 @@ def _fim_response_bytes():
"object": "text_completion",
"created": 1,
"model": "mercury-edit-2",
"choices": [
{"text": "a + b", "index": 0, "finish_reason": "stop", "logprobs": None}
],
"choices": [{"text": "a + b", "index": 0, "finish_reason": "stop", "logprobs": None}],
"usage": {"prompt_tokens": 5, "completion_tokens": 3, "total_tokens": 8},
}
).encode()
@ -47,9 +45,7 @@ def test_inception_fim_supports_suffix_param():
def test_inception_fim_supported_params_match_schema():
"""FIM exposes the OpenAI subset of Inception's FIMCompletionRequest only"""
params = InceptionTextCompletionConfig().get_supported_openai_params(
"mercury-edit-2"
)
params = InceptionTextCompletionConfig().get_supported_openai_params("mercury-edit-2")
for p in ("suffix", "top_p", "frequency_penalty", "presence_penalty", "stop"):
assert p in params
# Chat-only sampling controls are not part of Inception's FIM schema
@ -75,11 +71,7 @@ def test_inception_get_supported_openai_params_dispatch():
@pytest.mark.parametrize("provider", ["inception", "text-completion-inception"])
def test_inception_validate_environment(provider):
model = (
"inception/mercury-2"
if provider == "inception"
else "text-completion-inception/mercury-edit-2"
)
model = "inception/mercury-2" if provider == "inception" else "text-completion-inception/mercury-edit-2"
with mock.patch.dict(os.environ, {}, clear=True):
result = litellm.validate_environment(model)
@ -217,9 +209,7 @@ def test_inception_fim_does_not_leak_global_api_key():
content=_fim_response_bytes(),
)
with mock.patch.dict(
os.environ, {"INCEPTION_API_KEY": "sk-inception-correct"}, clear=True
):
with mock.patch.dict(os.environ, {"INCEPTION_API_KEY": "sk-inception-correct"}, clear=True):
with mock.patch.object(litellm, "inception_key", None):
with mock.patch.object(litellm, "api_key", "sk-global-should-not-leak"):
with mock.patch("httpx.Client.send", new=fake_send):

View file

@ -46,7 +46,7 @@ def test_langflow_config_get_complete_url():
def test_langflow_config_get_complete_url_requires_api_base():
config = LangFlowConfig()
with pytest.raises(ValueError, match='api_base is required for LangFlow\\. Set it via'):
with pytest.raises(ValueError, match="api_base is required for LangFlow\\. Set it via"):
config.get_complete_url(
api_base=None,
api_key=None,
@ -225,9 +225,7 @@ def test_langflow_extra_body_cannot_inject_tweaks_into_run_payload():
posted_bodies.append(json.loads(body) if isinstance(body, str) else body)
resp = MagicMock(spec=httpx.Response)
resp.status_code = 200
resp.json.return_value = {
"outputs": [{"outputs": [{"results": {"message": {"text": "hi"}}}]}]
}
resp.json.return_value = {"outputs": [{"outputs": [{"results": {"message": {"text": "hi"}}}]}]}
resp.headers = {}
resp.text = "{}"
return resp
@ -275,9 +273,7 @@ def test_langflow_config_extract_response_from_outputs_dict():
"outputs": [
{
"results": {},
"outputs": {
"message": {"message": {"text": "via outputs dict"}}
},
"outputs": {"message": {"message": {"text": "via outputs dict"}}},
}
]
}
@ -292,14 +288,9 @@ def test_langflow_extract_response_returns_none_when_no_message():
assert config._extract_content_from_response({"outputs": []}) is None
assert config._extract_content_from_response({"detail": "flow failed"}) is None
assert config._extract_content_from_response({"outputs": ["not-a-dict"]}) is None
assert config._extract_content_from_response({"outputs": [{"outputs": ["bad"]}]}) is None
assert (
config._extract_content_from_response({"outputs": [{"outputs": ["bad"]}]})
is None
)
assert (
config._extract_content_from_response(
{"outputs": [{"outputs": [{"results": {"message": {"text": ""}}}]}]}
)
config._extract_content_from_response({"outputs": [{"outputs": [{"results": {"message": {"text": ""}}}]}]})
is None
)
@ -310,9 +301,7 @@ def test_langflow_transform_response_builds_model_response_with_usage():
status_code=200,
json={
"session_id": "sess-abc",
"outputs": [
{"outputs": [{"results": {"message": {"text": "Hello from LangFlow"}}}]}
],
"outputs": [{"outputs": [{"results": {"message": {"text": "Hello from LangFlow"}}}]}],
},
)
@ -332,9 +321,7 @@ def test_langflow_transform_response_builds_model_response_with_usage():
assert result.choices[0].finish_reason == "stop"
assert result.model == "langflow/my-flow-id"
assert result.usage.completion_tokens > 0
assert result.usage.total_tokens == (
result.usage.prompt_tokens + result.usage.completion_tokens
)
assert result.usage.total_tokens == (result.usage.prompt_tokens + result.usage.completion_tokens)
def test_langflow_transform_response_raises_on_unparseable_body():
@ -357,9 +344,7 @@ def test_langflow_transform_response_raises_on_unparseable_body():
def test_langflow_transform_response_raises_on_non_json_body():
config = LangFlowConfig()
raw_response = httpx.Response(
status_code=200, content=b"not json", headers={"content-type": "text/plain"}
)
raw_response = httpx.Response(status_code=200, content=b"not json", headers={"content-type": "text/plain"})
with pytest.raises(LangFlowError):
config.transform_response(

View file

@ -55,9 +55,7 @@ def _install_fake_sandbox(monkeypatch, session_cls=_FakeSandboxSession):
def test_execute_installs_inline_requirements_file(monkeypatch):
_install_fake_sandbox(monkeypatch)
executor = SkillsSandboxExecutor()
monkeypatch.setattr(
executor, "_collect_generated_files", lambda *args, **kwargs: []
)
monkeypatch.setattr(executor, "_collect_generated_files", lambda *args, **kwargs: [])
requirements = "git+https://example.com/repo.git#egg=foo\n-r extra.txt\n-e ./pkg\n"
result = executor.execute(
@ -69,22 +67,15 @@ def test_execute_installs_inline_requirements_file(monkeypatch):
assert result["success"] is True
created_session = _FakeSandboxSession.last_instance
assert created_session.copied_contents[
"/sandbox/.litellm_requirements.txt"
] == requirements.encode("utf-8")
assert (
"pip', 'install', '-r', '.litellm_requirements.txt'"
in created_session.run_calls[0]
)
assert created_session.copied_contents["/sandbox/.litellm_requirements.txt"] == requirements.encode("utf-8")
assert "pip', 'install', '-r', '.litellm_requirements.txt'" in created_session.run_calls[0]
assert "os.chdir('/sandbox')" in created_session.run_calls[1]
def test_execute_uses_skill_requirements_txt(monkeypatch):
_install_fake_sandbox(monkeypatch)
executor = SkillsSandboxExecutor()
monkeypatch.setattr(
executor, "_collect_generated_files", lambda *args, **kwargs: []
)
monkeypatch.setattr(executor, "_collect_generated_files", lambda *args, **kwargs: [])
result = executor.execute(
code="print('hello')",
@ -97,9 +88,7 @@ def test_execute_uses_skill_requirements_txt(monkeypatch):
assert result["success"] is True
created_session = _FakeSandboxSession.last_instance
copied_paths = {
sandbox_path for _, sandbox_path in created_session.copy_to_runtime_calls
}
copied_paths = {sandbox_path for _, sandbox_path in created_session.copy_to_runtime_calls}
assert "/sandbox/requirements.txt" in copied_paths
assert "/sandbox/.litellm_requirements.txt" not in copied_paths
assert "pip', 'install', '-r', 'requirements.txt'" in created_session.run_calls[0]
@ -118,9 +107,7 @@ def test_execute_returns_install_failure(monkeypatch):
_install_fake_sandbox(monkeypatch, session_cls=_FailingSandboxSession)
executor = SkillsSandboxExecutor()
monkeypatch.setattr(
executor, "_collect_generated_files", lambda *args, **kwargs: []
)
monkeypatch.setattr(executor, "_collect_generated_files", lambda *args, **kwargs: [])
result = executor.execute(
code="print('hello')",

View file

@ -37,12 +37,7 @@ def _skill(skill_id: str, created_by: str | None) -> LiteLLM_SkillsTable:
def test_should_extract_skill_auth_from_supported_metadata_fields():
auth = UserAPIKeyAuth(user_id="user-1")
assert (
skills_main._get_user_api_key_auth_from_kwargs(
{"metadata": {"user_api_key_auth": auth}}
)
is auth
)
assert skills_main._get_user_api_key_auth_from_kwargs({"metadata": {"user_api_key_auth": auth}}) is auth
assert (
skills_main._get_user_api_key_auth_from_kwargs(
{"metadata": {}, "litellm_metadata": {"user_api_key_auth": auth}}
@ -122,9 +117,7 @@ def test_should_forward_skill_auth_through_sdk_entrypoints(monkeypatch):
== "deleted"
)
assert handler.create_skill_handler.call_args.kwargs["metadata"] == {
"source": "request"
}
assert handler.create_skill_handler.call_args.kwargs["metadata"] == {"source": "request"}
assert handler.create_skill_handler.call_args.kwargs["user_api_key_dict"] is auth
assert handler.list_skills_handler.call_args.kwargs["user_api_key_dict"] is auth
assert handler.get_skill_handler.call_args.kwargs["user_api_key_dict"] is auth
@ -149,9 +142,7 @@ def test_should_build_resource_owner_scopes_for_auth_context():
]
assert resource_ownership.get_primary_resource_owner_scope(auth) == "user-1"
assert resource_ownership.user_can_access_resource_owner("team:team-1", auth)
assert resource_ownership.get_resource_owner_scopes(
UserAPIKeyAuth(token="token-hash")
) == ["key:token-hash"]
assert resource_ownership.get_resource_owner_scopes(UserAPIKeyAuth(token="token-hash")) == ["key:token-hash"]
# Identity-less callers get an empty scope set — sharing a sentinel
# would collapse every identity-less caller into the same logical
# owner, which is a cross-tenant data-access primitive.
@ -165,9 +156,7 @@ def test_should_allow_admin_and_anonymous_resource_owner_paths():
assert resource_ownership.is_proxy_admin(admin)
assert resource_ownership.user_can_access_resource_owner(None, admin)
assert resource_ownership.user_can_access_resource_owner(None, None)
assert not resource_ownership.user_can_access_resource_owner(
None, UserAPIKeyAuth(user_id="user-1")
)
assert not resource_ownership.user_can_access_resource_owner(None, UserAPIKeyAuth(user_id="user-1"))
@pytest.mark.asyncio
@ -218,9 +207,7 @@ async def test_should_forward_skill_auth_through_transformation_handler(monkeypa
async def test_should_store_team_owner_for_keys_without_user_id(monkeypatch):
table = AsyncMock()
table.create.side_effect = lambda data: _skill(data["skill_id"], data["created_by"])
prisma_client = type(
"Prisma", (), {"db": type("DB", (), {"litellm_skillstable": table})()}
)()
prisma_client = type("Prisma", (), {"db": type("DB", (), {"litellm_skillstable": table})()})()
monkeypatch.setattr(
LiteLLMSkillsHandler,
"_get_prisma_client",
@ -242,9 +229,7 @@ async def test_should_store_team_owner_for_keys_without_user_id(monkeypatch):
async def test_should_store_token_owner_for_keys_without_user_team_or_org(monkeypatch):
table = AsyncMock()
table.create.side_effect = lambda data: _skill(data["skill_id"], data["created_by"])
prisma_client = type(
"Prisma", (), {"db": type("DB", (), {"litellm_skillstable": table})()}
)()
prisma_client = type("Prisma", (), {"db": type("DB", (), {"litellm_skillstable": table})()})()
monkeypatch.setattr(
LiteLLMSkillsHandler,
"_get_prisma_client",
@ -268,9 +253,7 @@ async def test_should_reject_skill_create_for_identityless_proxy_auth(monkeypatc
sentinel as ``created_by`` would let any two such callers see each
other's skills via the resulting shared owner scope."""
table = AsyncMock()
prisma_client = type(
"Prisma", (), {"db": type("DB", (), {"litellm_skillstable": table})()}
)()
prisma_client = type("Prisma", (), {"db": type("DB", (), {"litellm_skillstable": table})()})()
monkeypatch.setattr(
LiteLLMSkillsHandler,
"_get_prisma_client",
@ -291,9 +274,7 @@ async def test_should_reject_skill_create_for_identityless_proxy_auth(monkeypatc
async def test_should_filter_list_skills_to_authenticated_owner_scopes(monkeypatch):
table = AsyncMock()
table.find_many.return_value = [_skill("litellm_skill_owner", "user-1")]
prisma_client = type(
"Prisma", (), {"db": type("DB", (), {"litellm_skillstable": table})()}
)()
prisma_client = type("Prisma", (), {"db": type("DB", (), {"litellm_skillstable": table})()})()
monkeypatch.setattr(
LiteLLMSkillsHandler,
"_get_prisma_client",
@ -318,9 +299,7 @@ async def test_should_filter_list_skills_to_authenticated_owner_scopes(monkeypat
async def test_should_hide_skill_from_different_owner(monkeypatch):
table = AsyncMock()
table.find_unique.return_value = _skill("litellm_skill_other", "user-2")
prisma_client = type(
"Prisma", (), {"db": type("DB", (), {"litellm_skillstable": table})()}
)()
prisma_client = type("Prisma", (), {"db": type("DB", (), {"litellm_skillstable": table})()})()
monkeypatch.setattr(
LiteLLMSkillsHandler,
"_get_prisma_client",
@ -340,9 +319,7 @@ async def test_should_hide_skill_from_different_owner(monkeypatch):
async def test_should_hide_unowned_skill_by_default(monkeypatch):
table = AsyncMock()
table.find_unique.return_value = _skill("litellm_skill_unowned", None)
prisma_client = type(
"Prisma", (), {"db": type("DB", (), {"litellm_skillstable": table})()}
)()
prisma_client = type("Prisma", (), {"db": type("DB", (), {"litellm_skillstable": table})()})()
monkeypatch.setattr(
LiteLLMSkillsHandler,
"_get_prisma_client",
@ -364,9 +341,7 @@ async def test_list_skills_excludes_unowned_for_non_admin(monkeypatch):
with ``created_by IS NULL`` are excluded — admin-only."""
table = AsyncMock()
table.find_many.return_value = []
prisma_client = type(
"Prisma", (), {"db": type("DB", (), {"litellm_skillstable": table})()}
)()
prisma_client = type("Prisma", (), {"db": type("DB", (), {"litellm_skillstable": table})()})()
monkeypatch.setattr(
LiteLLMSkillsHandler,
"_get_prisma_client",
@ -422,9 +397,7 @@ async def test_load_skill_uses_cache_after_first_db_hit(monkeypatch):
fake_skill = Mock(created_by="user-1", skill_id="litellm_skill_a")
table = AsyncMock()
table.find_unique = AsyncMock(return_value=fake_skill)
prisma_client = type(
"Prisma", (), {"db": type("DB", (), {"litellm_skillstable": table})()}
)()
prisma_client = type("Prisma", (), {"db": type("DB", (), {"litellm_skillstable": table})()})()
monkeypatch.setattr(
skills_handler.LiteLLMSkillsHandler,
"_get_prisma_client",
@ -432,10 +405,7 @@ async def test_load_skill_uses_cache_after_first_db_hit(monkeypatch):
)
for _ in range(3):
assert (
await skills_handler.LiteLLMSkillsHandler._load_skill("litellm_skill_a")
is fake_skill
)
assert await skills_handler.LiteLLMSkillsHandler._load_skill("litellm_skill_a") is fake_skill
assert table.find_unique.await_count == 1
@ -445,9 +415,7 @@ async def test_load_skill_caches_negative_lookups(monkeypatch):
the DB and the caller still sees ``None``."""
table = AsyncMock()
table.find_unique = AsyncMock(return_value=None)
prisma_client = type(
"Prisma", (), {"db": type("DB", (), {"litellm_skillstable": table})()}
)()
prisma_client = type("Prisma", (), {"db": type("DB", (), {"litellm_skillstable": table})()})()
monkeypatch.setattr(
skills_handler.LiteLLMSkillsHandler,
"_get_prisma_client",
@ -466,9 +434,7 @@ async def test_delete_skill_invalidates_cache(monkeypatch):
table = AsyncMock()
table.find_unique = AsyncMock(return_value=fake_skill)
table.delete = AsyncMock()
prisma_client = type(
"Prisma", (), {"db": type("DB", (), {"litellm_skillstable": table})()}
)()
prisma_client = type("Prisma", (), {"db": type("DB", (), {"litellm_skillstable": table})()})()
monkeypatch.setattr(
skills_handler.LiteLLMSkillsHandler,
"_get_prisma_client",
@ -480,12 +446,7 @@ async def test_delete_skill_invalidates_cache(monkeypatch):
assert skills_handler._SKILL_CACHE.get_cache("litellm_skill_a") is fake_skill
auth = UserAPIKeyAuth(user_id="user-1")
await skills_handler.LiteLLMSkillsHandler.delete_skill(
"litellm_skill_a", user_api_key_dict=auth
)
await skills_handler.LiteLLMSkillsHandler.delete_skill("litellm_skill_a", user_api_key_dict=auth)
# Post-delete, the cache holds the negative sentinel — not the stale row.
assert (
skills_handler._SKILL_CACHE.get_cache("litellm_skill_a")
== skills_handler._NEGATIVE_SKILL_SENTINEL
)
assert skills_handler._SKILL_CACHE.get_cache("litellm_skill_a") == skills_handler._NEGATIVE_SKILL_SENTINEL