mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-21 00:21:49 +00:00
feat: keep prompts only in s3 bucket aws
This commit is contained in:
parent
0f01802dde
commit
f08f81312f
4 changed files with 243 additions and 86 deletions
|
|
@ -62,6 +62,11 @@ class S3Logger:
|
|||
s3_use_team_prefix = bool(
|
||||
litellm.s3_callback_params.get("s3_use_team_prefix", False)
|
||||
)
|
||||
self.s3_log_prompts_only = bool(
|
||||
litellm.s3_callback_params.get("s3_log_prompts_only", False)
|
||||
)
|
||||
else:
|
||||
self.s3_log_prompts_only = False
|
||||
self.s3_use_team_prefix = s3_use_team_prefix
|
||||
self.bucket_name = s3_bucket_name
|
||||
self.s3_path = s3_path
|
||||
|
|
@ -156,7 +161,13 @@ class S3Logger:
|
|||
|
||||
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
|
||||
|
||||
payload_str = safe_dumps(payload)
|
||||
payload_dict = dict(payload)
|
||||
if self.s3_log_prompts_only:
|
||||
# Store only prompt content when prompts-only logging is enabled.
|
||||
payload_dict = {
|
||||
"messages": payload_dict.get("messages") or [],
|
||||
}
|
||||
payload_str = safe_dumps(payload_dict)
|
||||
|
||||
print_verbose(f"\ns3 Logger - Logging payload = {payload_str}")
|
||||
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ class S3Logger(CustomBatchLogger, BaseAWSLLM):
|
|||
s3_use_team_prefix: bool = False,
|
||||
s3_strip_base64_files: bool = False,
|
||||
s3_use_key_prefix: bool = False,
|
||||
s3_log_prompts_only: bool = False,
|
||||
**kwargs,
|
||||
):
|
||||
try:
|
||||
|
|
@ -78,7 +79,8 @@ class S3Logger(CustomBatchLogger, BaseAWSLLM):
|
|||
s3_path=s3_path,
|
||||
s3_use_team_prefix=s3_use_team_prefix,
|
||||
s3_strip_base64_files=s3_strip_base64_files,
|
||||
s3_use_key_prefix=s3_use_key_prefix
|
||||
s3_use_key_prefix=s3_use_key_prefix,
|
||||
s3_log_prompts_only=s3_log_prompts_only,
|
||||
)
|
||||
verbose_logger.debug(f"s3 logger using endpoint url {s3_endpoint_url}")
|
||||
|
||||
|
|
@ -89,7 +91,7 @@ class S3Logger(CustomBatchLogger, BaseAWSLLM):
|
|||
)
|
||||
self.async_httpx_client = get_async_httpx_client(
|
||||
llm_provider=httpxSpecialProvider.LoggingCallback,
|
||||
params={"ssl_verify": self.s3_verify}
|
||||
params={"ssl_verify": self.s3_verify},
|
||||
)
|
||||
|
||||
asyncio.create_task(self.periodic_flush())
|
||||
|
|
@ -135,6 +137,7 @@ class S3Logger(CustomBatchLogger, BaseAWSLLM):
|
|||
s3_use_team_prefix: bool = False,
|
||||
s3_strip_base64_files: bool = False,
|
||||
s3_use_key_prefix: bool = False,
|
||||
s3_log_prompts_only: bool = False,
|
||||
):
|
||||
"""
|
||||
Initialize the s3 params for this logging callback
|
||||
|
|
@ -155,10 +158,14 @@ class S3Logger(CustomBatchLogger, BaseAWSLLM):
|
|||
litellm.s3_callback_params.get("s3_api_version") or s3_api_version
|
||||
)
|
||||
self.s3_use_ssl = (
|
||||
litellm.s3_callback_params.get("s3_use_ssl", True) if litellm.s3_callback_params.get("s3_use_ssl") is not None else s3_use_ssl
|
||||
litellm.s3_callback_params.get("s3_use_ssl", True)
|
||||
if litellm.s3_callback_params.get("s3_use_ssl") is not None
|
||||
else s3_use_ssl
|
||||
)
|
||||
self.s3_verify = (
|
||||
litellm.s3_callback_params.get("s3_verify") if litellm.s3_callback_params.get("s3_verify") is not None else s3_verify
|
||||
litellm.s3_callback_params.get("s3_verify")
|
||||
if litellm.s3_callback_params.get("s3_verify") is not None
|
||||
else s3_verify
|
||||
)
|
||||
self.s3_endpoint_url = (
|
||||
litellm.s3_callback_params.get("s3_endpoint_url") or s3_endpoint_url
|
||||
|
|
@ -208,8 +215,8 @@ class S3Logger(CustomBatchLogger, BaseAWSLLM):
|
|||
)
|
||||
|
||||
self.s3_use_key_prefix = (
|
||||
bool(litellm.s3_callback_params.get("s3_use_key_prefix", False))
|
||||
or s3_use_key_prefix
|
||||
bool(litellm.s3_callback_params.get("s3_use_key_prefix", False))
|
||||
or s3_use_key_prefix
|
||||
)
|
||||
|
||||
self.s3_strip_base64_files = (
|
||||
|
|
@ -217,6 +224,11 @@ class S3Logger(CustomBatchLogger, BaseAWSLLM):
|
|||
or s3_strip_base64_files
|
||||
)
|
||||
|
||||
self.s3_log_prompts_only = (
|
||||
bool(litellm.s3_callback_params.get("s3_log_prompts_only", False))
|
||||
or s3_log_prompts_only
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
async def async_log_success_event(self, kwargs, response_obj, start_time, end_time):
|
||||
|
|
@ -294,9 +306,7 @@ class S3Logger(CustomBatchLogger, BaseAWSLLM):
|
|||
verbose_logger.debug(
|
||||
f"s3_v2 logger - uploading data to s3 - {batch_logging_element.s3_object_key}"
|
||||
)
|
||||
verbose_logger.debug(
|
||||
f"s3_v2 logger - s3_verify setting: {self.s3_verify}"
|
||||
)
|
||||
verbose_logger.debug(f"s3_v2 logger - s3_verify setting: {self.s3_verify}")
|
||||
|
||||
# Prepare the URL
|
||||
url = f"https://{self.s3_bucket_name}.s3.{self.s3_region_name}.amazonaws.com/{batch_logging_element.s3_object_key}"
|
||||
|
|
@ -392,20 +402,25 @@ class S3Logger(CustomBatchLogger, BaseAWSLLM):
|
|||
return None
|
||||
|
||||
if self.s3_strip_base64_files:
|
||||
standard_logging_payload = self._strip_base64_from_messages_sync(standard_logging_payload)
|
||||
standard_logging_payload = self._strip_base64_from_messages_sync(
|
||||
standard_logging_payload
|
||||
)
|
||||
|
||||
# Base prefix (default empty)
|
||||
prefix_components = []
|
||||
if self.s3_use_team_prefix:
|
||||
team_alias = standard_logging_payload.get("metadata", {}).get("user_api_key_team_alias", None)
|
||||
team_alias = standard_logging_payload.get("metadata", {}).get(
|
||||
"user_api_key_team_alias", None
|
||||
)
|
||||
if team_alias:
|
||||
prefix_components.append(team_alias)
|
||||
if self.s3_use_key_prefix:
|
||||
user_api_key_alias = standard_logging_payload.get("metadata", {}).get("user_api_key_alias", None)
|
||||
user_api_key_alias = standard_logging_payload.get("metadata", {}).get(
|
||||
"user_api_key_alias", None
|
||||
)
|
||||
if user_api_key_alias:
|
||||
prefix_components.append(user_api_key_alias)
|
||||
|
||||
|
||||
# Construct full prefix path
|
||||
prefix_path = "/".join(prefix_components)
|
||||
if prefix_path:
|
||||
|
|
@ -414,7 +429,9 @@ class S3Logger(CustomBatchLogger, BaseAWSLLM):
|
|||
s3_file_name = (
|
||||
litellm.utils.get_logging_id(start_time, standard_logging_payload) or ""
|
||||
)
|
||||
verbose_logger.debug(f"Creating s3 file with prefix_components={prefix_components},prefix_path={prefix_path} and {s3_file_name}")
|
||||
verbose_logger.debug(
|
||||
f"Creating s3 file with prefix_components={prefix_components},prefix_path={prefix_path} and {s3_file_name}"
|
||||
)
|
||||
s3_object_key = get_s3_object_key(
|
||||
s3_path=cast(Optional[str], self.s3_path) or "",
|
||||
prefix=prefix_path,
|
||||
|
|
@ -425,8 +442,15 @@ class S3Logger(CustomBatchLogger, BaseAWSLLM):
|
|||
|
||||
s3_object_download_filename = f"time-{start_time.strftime('%Y-%m-%dT%H-%M-%S-%f')}_{standard_logging_payload['id']}.json"
|
||||
|
||||
payload_dict = dict(standard_logging_payload)
|
||||
if self.s3_log_prompts_only:
|
||||
# Store only prompt content when prompts-only logging is enabled.
|
||||
payload_dict = {
|
||||
"messages": payload_dict.get("messages") or [],
|
||||
}
|
||||
|
||||
return s3BatchLoggingElement(
|
||||
payload=dict(standard_logging_payload),
|
||||
payload=payload_dict,
|
||||
s3_object_key=s3_object_key,
|
||||
s3_object_download_filename=s3_object_download_filename,
|
||||
)
|
||||
|
|
@ -497,7 +521,9 @@ class S3Logger(CustomBatchLogger, BaseAWSLLM):
|
|||
signed_headers = dict(aws_request.headers.items())
|
||||
|
||||
httpx_client = _get_httpx_client(
|
||||
params={"ssl_verify": self.s3_verify} if self.s3_verify is not None else None
|
||||
params={"ssl_verify": self.s3_verify}
|
||||
if self.s3_verify is not None
|
||||
else None
|
||||
)
|
||||
# Make the request
|
||||
response = httpx_client.put(url, data=json_string, headers=signed_headers)
|
||||
|
|
|
|||
47
tests/test_litellm/integrations/test_s3.py
Normal file
47
tests/test_litellm/integrations/test_s3.py
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import json
|
||||
from datetime import datetime
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import litellm
|
||||
|
||||
from litellm.integrations.s3 import S3Logger
|
||||
|
||||
|
||||
def test_s3_prompts_only_payload():
|
||||
mock_client = MagicMock()
|
||||
mock_client.put_object.return_value = {"ResponseMetadata": {"HTTPStatusCode": 200}}
|
||||
|
||||
with patch("boto3.client", return_value=mock_client):
|
||||
litellm.s3_callback_params = {
|
||||
"s3_bucket_name": "test-bucket",
|
||||
"s3_region_name": "us-east-1",
|
||||
"s3_log_prompts_only": True,
|
||||
}
|
||||
|
||||
logger = S3Logger()
|
||||
|
||||
kwargs = {
|
||||
"standard_logging_object": {
|
||||
"id": "test-id",
|
||||
"metadata": {},
|
||||
"messages": [{"role": "user", "content": "hello"}],
|
||||
"response": {"id": "resp"},
|
||||
}
|
||||
}
|
||||
|
||||
logger.log_event(
|
||||
kwargs=kwargs,
|
||||
response_obj={},
|
||||
start_time=datetime(2026, 2, 10, 15, 0, 0),
|
||||
end_time=datetime(2026, 2, 10, 15, 0, 1),
|
||||
print_verbose=lambda *args, **kwargs: None,
|
||||
)
|
||||
|
||||
call_args = mock_client.put_object.call_args
|
||||
assert call_args is not None
|
||||
payload_str = call_args.kwargs["Body"]
|
||||
payload = json.loads(payload_str)
|
||||
|
||||
assert payload == {"messages": [{"role": "user", "content": "hello"}]}
|
||||
|
||||
litellm.s3_callback_params = None
|
||||
|
|
@ -25,11 +25,13 @@ class TestS3V2UnitTests:
|
|||
"json.dumps(" not in source_code
|
||||
), "S3 v2 should not use json.dumps directly"
|
||||
|
||||
@patch('asyncio.create_task')
|
||||
@patch('litellm.integrations.s3_v2.CustomBatchLogger.periodic_flush')
|
||||
def test_s3_v2_endpoint_url(self, mock_periodic_flush, mock_create_task):
|
||||
@patch("asyncio.create_task")
|
||||
@patch("litellm.integrations.s3_v2.CustomBatchLogger.periodic_flush")
|
||||
def test_s3_v2_endpoint_url( # noqa: PLR0915
|
||||
self, mock_periodic_flush, mock_create_task
|
||||
):
|
||||
"""testing s3 endpoint url"""
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from litellm.types.integrations.s3_v2 import s3BatchLoggingElement
|
||||
|
||||
|
|
@ -46,7 +48,7 @@ class TestS3V2UnitTests:
|
|||
test_element = s3BatchLoggingElement(
|
||||
s3_object_key="2025-09-14/test-key.json",
|
||||
payload={"test": "data"},
|
||||
s3_object_download_filename="test-file.json"
|
||||
s3_object_download_filename="test-file.json",
|
||||
)
|
||||
|
||||
# Test 1: Custom endpoint URL with bucket name
|
||||
|
|
@ -55,7 +57,7 @@ class TestS3V2UnitTests:
|
|||
s3_endpoint_url="https://s3.amazonaws.com",
|
||||
s3_aws_access_key_id="test-key",
|
||||
s3_aws_secret_access_key="test-secret",
|
||||
s3_region_name="us-east-1"
|
||||
s3_region_name="us-east-1",
|
||||
)
|
||||
|
||||
s3_logger.async_httpx_client = AsyncMock()
|
||||
|
|
@ -75,7 +77,7 @@ class TestS3V2UnitTests:
|
|||
s3_endpoint_url="https://minio.example.com:9000",
|
||||
s3_aws_access_key_id="minio-key",
|
||||
s3_aws_secret_access_key="minio-secret",
|
||||
s3_region_name="us-east-1"
|
||||
s3_region_name="us-east-1",
|
||||
)
|
||||
|
||||
s3_logger_minio.async_httpx_client = AsyncMock()
|
||||
|
|
@ -86,15 +88,19 @@ class TestS3V2UnitTests:
|
|||
call_args_minio = s3_logger_minio.async_httpx_client.put.call_args
|
||||
assert call_args_minio is not None
|
||||
url_minio = call_args_minio[0][0]
|
||||
expected_minio_url = "https://minio.example.com:9000/litellm-logs/2025-09-14/test-key.json"
|
||||
assert url_minio == expected_minio_url, f"Expected MinIO URL {expected_minio_url}, got {url_minio}"
|
||||
expected_minio_url = (
|
||||
"https://minio.example.com:9000/litellm-logs/2025-09-14/test-key.json"
|
||||
)
|
||||
assert (
|
||||
url_minio == expected_minio_url
|
||||
), f"Expected MinIO URL {expected_minio_url}, got {url_minio}"
|
||||
|
||||
# Test 3: Custom endpoint without bucket name (should fall back to default)
|
||||
s3_logger_no_bucket = S3Logger(
|
||||
s3_endpoint_url="https://s3.amazonaws.com",
|
||||
s3_aws_access_key_id="test-key",
|
||||
s3_aws_secret_access_key="test-secret",
|
||||
s3_region_name="us-east-1"
|
||||
s3_region_name="us-east-1",
|
||||
)
|
||||
|
||||
s3_logger_no_bucket.async_httpx_client = AsyncMock()
|
||||
|
|
@ -117,20 +123,27 @@ class TestS3V2UnitTests:
|
|||
s3_endpoint_url="https://custom.s3.endpoint.com",
|
||||
s3_aws_access_key_id="sync-key",
|
||||
s3_aws_secret_access_key="sync-secret",
|
||||
s3_region_name="us-east-1"
|
||||
s3_region_name="us-east-1",
|
||||
)
|
||||
|
||||
mock_sync_client = MagicMock()
|
||||
mock_sync_client.put.return_value = mock_response
|
||||
|
||||
with patch('litellm.integrations.s3_v2._get_httpx_client', return_value=mock_sync_client):
|
||||
with patch(
|
||||
"litellm.integrations.s3_v2._get_httpx_client",
|
||||
return_value=mock_sync_client,
|
||||
):
|
||||
s3_logger_sync.upload_data_to_s3(test_element)
|
||||
|
||||
call_args_sync = mock_sync_client.put.call_args
|
||||
assert call_args_sync is not None
|
||||
url_sync = call_args_sync[0][0]
|
||||
expected_sync_url = "https://custom.s3.endpoint.com/sync-bucket/2025-09-14/test-key.json"
|
||||
assert url_sync == expected_sync_url, f"Expected sync URL {expected_sync_url}, got {url_sync}"
|
||||
expected_sync_url = (
|
||||
"https://custom.s3.endpoint.com/sync-bucket/2025-09-14/test-key.json"
|
||||
)
|
||||
assert (
|
||||
url_sync == expected_sync_url
|
||||
), f"Expected sync URL {expected_sync_url}, got {url_sync}"
|
||||
|
||||
# Test 5: Download method with custom endpoint
|
||||
s3_logger_download = S3Logger(
|
||||
|
|
@ -138,7 +151,7 @@ class TestS3V2UnitTests:
|
|||
s3_endpoint_url="https://download.s3.endpoint.com",
|
||||
s3_aws_access_key_id="download-key",
|
||||
s3_aws_secret_access_key="download-secret",
|
||||
s3_region_name="us-east-1"
|
||||
s3_region_name="us-east-1",
|
||||
)
|
||||
|
||||
mock_download_response = MagicMock()
|
||||
|
|
@ -147,16 +160,23 @@ class TestS3V2UnitTests:
|
|||
s3_logger_download.async_httpx_client = AsyncMock()
|
||||
s3_logger_download.async_httpx_client.get.return_value = mock_download_response
|
||||
|
||||
result = asyncio.run(s3_logger_download._download_object_from_s3("2025-09-14/download-test-key.json"))
|
||||
result = asyncio.run(
|
||||
s3_logger_download._download_object_from_s3(
|
||||
"2025-09-14/download-test-key.json"
|
||||
)
|
||||
)
|
||||
|
||||
call_args_download = s3_logger_download.async_httpx_client.get.call_args
|
||||
assert call_args_download is not None
|
||||
url_download = call_args_download[0][0]
|
||||
expected_download_url = "https://download.s3.endpoint.com/download-bucket/2025-09-14/download-test-key.json"
|
||||
assert url_download == expected_download_url, f"Expected download URL {expected_download_url}, got {url_download}"
|
||||
assert (
|
||||
url_download == expected_download_url
|
||||
), f"Expected download URL {expected_download_url}, got {url_download}"
|
||||
|
||||
assert result == {"downloaded": "data"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_strip_base64_removes_file_and_nontext_entries():
|
||||
logger = S3Logger(s3_strip_base64_files=True)
|
||||
|
|
@ -167,15 +187,24 @@ async def test_strip_base64_removes_file_and_nontext_entries():
|
|||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "Hello world"},
|
||||
{"type": "image", "file": {"file_data": "data:image/png;base64,AAAA"}},
|
||||
{"type": "file", "file": {"file_data": "data:application/pdf;base64,BBBB"}},
|
||||
{
|
||||
"type": "image",
|
||||
"file": {"file_data": "data:image/png;base64,AAAA"},
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"file": {"file_data": "data:application/pdf;base64,BBBB"},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{"type": "text", "text": "Response"},
|
||||
{"type": "audio", "file": {"file_data": "data:audio/wav;base64,CCCC"}},
|
||||
{
|
||||
"type": "audio",
|
||||
"file": {"file_data": "data:audio/wav;base64,CCCC"},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
|
@ -267,11 +296,31 @@ async def test_strip_base64_mixed_nested_objects():
|
|||
assert stripped["messages"][0]["extra"]["trace_id"] == "123"
|
||||
|
||||
|
||||
def test_s3_v2_prompts_only_payload():
|
||||
start_time = datetime(2026, 2, 10, 15, 0, 0)
|
||||
logger = S3Logger(s3_log_prompts_only=True)
|
||||
|
||||
standard_logging_payload = {
|
||||
"id": "test-id",
|
||||
"metadata": {},
|
||||
"messages": [{"role": "user", "content": "hello"}],
|
||||
"response": {"id": "resp"},
|
||||
}
|
||||
|
||||
element = logger.create_s3_batch_logging_element(
|
||||
start_time=start_time,
|
||||
standard_logging_payload=standard_logging_payload,
|
||||
)
|
||||
|
||||
assert element is not None
|
||||
assert element.payload == {"messages": [{"role": "user", "content": "hello"}]}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_s3_verify_false_handling():
|
||||
"""
|
||||
Test that s3_verify=False is properly handled and not treated as None.
|
||||
|
||||
|
||||
This is a regression test for the bug where s3_verify=False was being
|
||||
ignored because 'False or s3_verify' would evaluate to s3_verify (None).
|
||||
"""
|
||||
|
|
@ -289,25 +338,35 @@ async def test_s3_verify_false_handling():
|
|||
"s3_verify": False, # This should NOT be ignored
|
||||
"s3_use_ssl": False, # This should also NOT be ignored
|
||||
}
|
||||
|
||||
with patch('asyncio.create_task'):
|
||||
with patch('litellm.integrations.s3_v2.get_async_httpx_client') as mock_get_client:
|
||||
|
||||
with patch("asyncio.create_task"):
|
||||
with patch(
|
||||
"litellm.integrations.s3_v2.get_async_httpx_client"
|
||||
) as mock_get_client:
|
||||
mock_client = AsyncMock()
|
||||
mock_get_client.return_value = mock_client
|
||||
|
||||
|
||||
# Create logger
|
||||
logger = S3Logger()
|
||||
|
||||
|
||||
# Verify s3_verify is False, not None
|
||||
assert logger.s3_verify is False, f"Expected s3_verify=False, got {logger.s3_verify}"
|
||||
assert logger.s3_use_ssl is False, f"Expected s3_use_ssl=False, got {logger.s3_use_ssl}"
|
||||
|
||||
assert (
|
||||
logger.s3_verify is False
|
||||
), f"Expected s3_verify=False, got {logger.s3_verify}"
|
||||
assert (
|
||||
logger.s3_use_ssl is False
|
||||
), f"Expected s3_use_ssl=False, got {logger.s3_use_ssl}"
|
||||
|
||||
# Verify that get_async_httpx_client was called with ssl_verify=False
|
||||
mock_get_client.assert_called_once()
|
||||
call_kwargs = mock_get_client.call_args.kwargs
|
||||
assert 'params' in call_kwargs, "params should be passed to get_async_httpx_client"
|
||||
assert call_kwargs['params'] == {'ssl_verify': False}, f"Expected ssl_verify=False in params, got {call_kwargs.get('params')}"
|
||||
|
||||
assert (
|
||||
"params" in call_kwargs
|
||||
), "params should be passed to get_async_httpx_client"
|
||||
assert call_kwargs["params"] == {
|
||||
"ssl_verify": False
|
||||
}, f"Expected ssl_verify=False in params, got {call_kwargs.get('params')}"
|
||||
|
||||
# Clean up
|
||||
litellm.s3_callback_params = None
|
||||
|
||||
|
|
@ -328,27 +387,31 @@ async def test_s3_verify_none_handling():
|
|||
"s3_aws_secret_access_key": "test-secret",
|
||||
"s3_region_name": "us-east-1",
|
||||
}
|
||||
|
||||
with patch('asyncio.create_task'):
|
||||
with patch('litellm.integrations.s3_v2.get_async_httpx_client') as mock_get_client:
|
||||
|
||||
with patch("asyncio.create_task"):
|
||||
with patch(
|
||||
"litellm.integrations.s3_v2.get_async_httpx_client"
|
||||
) as mock_get_client:
|
||||
mock_client = AsyncMock()
|
||||
mock_get_client.return_value = mock_client
|
||||
|
||||
|
||||
# Create logger without explicit s3_verify
|
||||
logger = S3Logger()
|
||||
|
||||
|
||||
# Verify s3_verify is None (default)
|
||||
assert logger.s3_verify is None, f"Expected s3_verify=None, got {logger.s3_verify}"
|
||||
|
||||
assert (
|
||||
logger.s3_verify is None
|
||||
), f"Expected s3_verify=None, got {logger.s3_verify}"
|
||||
|
||||
# Verify that get_async_httpx_client was called
|
||||
mock_get_client.assert_called_once()
|
||||
call_kwargs = mock_get_client.call_args.kwargs
|
||||
# When s3_verify is None, params={'ssl_verify': None} which is fine - uses default behavior
|
||||
# The important thing is it's not False
|
||||
if 'params' in call_kwargs and call_kwargs['params'] is not None:
|
||||
assert call_kwargs['params'].get('ssl_verify') is None
|
||||
if "params" in call_kwargs and call_kwargs["params"] is not None:
|
||||
assert call_kwargs["params"].get("ssl_verify") is None
|
||||
# Either params is None or params={'ssl_verify': None} is acceptable
|
||||
|
||||
|
||||
# Clean up
|
||||
litellm.s3_callback_params = None
|
||||
|
||||
|
|
@ -357,7 +420,7 @@ async def test_s3_verify_none_handling():
|
|||
async def test_s3_verify_false_creates_httpx_client_with_verify_false():
|
||||
"""
|
||||
Test that when s3_verify=False, the actual httpx client has verify=False.
|
||||
|
||||
|
||||
This validates that ssl_verify=False flows through to the httpx.AsyncClient.
|
||||
"""
|
||||
from unittest.mock import patch
|
||||
|
|
@ -373,22 +436,24 @@ async def test_s3_verify_false_creates_httpx_client_with_verify_false():
|
|||
"s3_region_name": "us-east-1",
|
||||
"s3_verify": False,
|
||||
}
|
||||
|
||||
with patch('asyncio.create_task'):
|
||||
|
||||
with patch("asyncio.create_task"):
|
||||
# Create logger - this creates the httpx client
|
||||
logger = S3Logger()
|
||||
|
||||
|
||||
# Verify the logger has s3_verify=False
|
||||
assert logger.s3_verify is False
|
||||
|
||||
|
||||
# Check the actual httpx client has verify=False
|
||||
# The async_httpx_client.client is the actual httpx.AsyncClient
|
||||
if hasattr(logger.async_httpx_client, 'client'):
|
||||
if hasattr(logger.async_httpx_client, "client"):
|
||||
httpx_client = logger.async_httpx_client.client
|
||||
# Check the _verify attribute (httpx internal)
|
||||
if hasattr(httpx_client, '_verify'):
|
||||
assert httpx_client._verify is False, f"Expected httpx client _verify=False, got {httpx_client._verify}"
|
||||
|
||||
if hasattr(httpx_client, "_verify"):
|
||||
assert (
|
||||
httpx_client._verify is False
|
||||
), f"Expected httpx client _verify=False, got {httpx_client._verify}"
|
||||
|
||||
# Clean up
|
||||
litellm.s3_callback_params = None
|
||||
|
||||
|
|
@ -398,7 +463,7 @@ async def test_s3_verify_false_async_client():
|
|||
"""
|
||||
Test that the async httpx client respects s3_verify=False.
|
||||
"""
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import litellm
|
||||
from litellm.types.integrations.s3_v2 import s3BatchLoggingElement
|
||||
|
|
@ -412,38 +477,40 @@ async def test_s3_verify_false_async_client():
|
|||
"s3_region_name": "us-east-1",
|
||||
"s3_verify": False,
|
||||
}
|
||||
|
||||
with patch('asyncio.create_task'):
|
||||
|
||||
with patch("asyncio.create_task"):
|
||||
logger = S3Logger()
|
||||
|
||||
|
||||
# Verify s3_verify is False
|
||||
assert logger.s3_verify is False
|
||||
|
||||
|
||||
# Create test element
|
||||
test_element = s3BatchLoggingElement(
|
||||
s3_object_key="2025-11-03/test-key.json",
|
||||
payload={"test": "data"},
|
||||
s3_object_download_filename="test-file.json"
|
||||
s3_object_download_filename="test-file.json",
|
||||
)
|
||||
|
||||
|
||||
# Mock the async httpx client's put method
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
logger.async_httpx_client.put = AsyncMock(return_value=mock_response)
|
||||
|
||||
|
||||
# Call async upload
|
||||
await logger.async_upload_data_to_s3(test_element)
|
||||
|
||||
|
||||
# Verify put was called
|
||||
assert logger.async_httpx_client.put.called
|
||||
|
||||
|
||||
# Check that the async httpx client was created with verify=False
|
||||
if hasattr(logger.async_httpx_client, 'client'):
|
||||
if hasattr(logger.async_httpx_client, "client"):
|
||||
httpx_client = logger.async_httpx_client.client
|
||||
if hasattr(httpx_client, '_verify'):
|
||||
assert httpx_client._verify is False, f"Expected async httpx client _verify=False, got {httpx_client._verify}"
|
||||
|
||||
if hasattr(httpx_client, "_verify"):
|
||||
assert (
|
||||
httpx_client._verify is False
|
||||
), f"Expected async httpx client _verify=False, got {httpx_client._verify}"
|
||||
|
||||
# Clean up
|
||||
litellm.s3_callback_params = None
|
||||
|
||||
|
|
@ -456,8 +523,14 @@ async def test_strip_base64_recursive_redaction():
|
|||
{
|
||||
"content": [
|
||||
{"type": "text", "text": "normal text"},
|
||||
{"type": "text", "text": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg"},
|
||||
{"type": "text", "text": "Nested: {'data': 'data:application/pdf;base64,AAA...'}"},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg",
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Nested: {'data': 'data:application/pdf;base64,AAA...'}",
|
||||
},
|
||||
{"file": {"file_data": "data:application/pdf;base64,AAAA"}},
|
||||
{"metadata": {"preview": "data:audio/mp3;base64,AAAAA=="}},
|
||||
]
|
||||
|
|
@ -473,6 +546,7 @@ async def test_strip_base64_recursive_redaction():
|
|||
|
||||
# Base64 redacted globally
|
||||
import json
|
||||
|
||||
for c in content:
|
||||
if isinstance(c, dict):
|
||||
s = json.dumps(c).lower()
|
||||
|
|
@ -480,7 +554,6 @@ async def test_strip_base64_recursive_redaction():
|
|||
assert "base64," not in s, f"Found real base64 blob in: {s}"
|
||||
|
||||
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# Shared fixture that silences asyncio.create_task during tests
|
||||
# --------------------------------------------------------------
|
||||
|
|
@ -507,7 +580,7 @@ def patch_asyncio_create_task():
|
|||
],
|
||||
)
|
||||
def test_s3_object_key_prefix_combinations(
|
||||
use_team_prefix, use_key_prefix, team_alias, key_alias, expected_prefix
|
||||
use_team_prefix, use_key_prefix, team_alias, key_alias, expected_prefix
|
||||
):
|
||||
"""
|
||||
Validate correct S3 prefix composition for team alias + key alias combinations.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue