mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-24 00:52:24 +00:00
test: migrate wave 1 phase 8 legacy llm tests to tests/unit
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
6ef7b86748
commit
4defed7f2e
20 changed files with 84 additions and 616 deletions
|
|
@ -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"])
|
||||
|
|
@ -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"])
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
@ -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):
|
||||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
@ -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"
|
||||
|
|
@ -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"]
|
||||
|
||||
|
|
@ -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():
|
||||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
||||
|
||||
|
|
@ -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):
|
||||
|
|
@ -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(
|
||||
|
|
@ -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')",
|
||||
|
|
@ -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
|
||||
Loading…
Add table
Reference in a new issue