diff --git a/litellm/integrations/s3_v2.py b/litellm/integrations/s3_v2.py index 405bf9698cc..91955655118 100644 --- a/litellm/integrations/s3_v2.py +++ b/litellm/integrations/s3_v2.py @@ -7,6 +7,7 @@ NOTE 1: S3 does not provide a BATCH PUT API endpoint, so we create tasks to uplo """ import asyncio +import re from datetime import datetime from typing import List, Optional, cast @@ -472,8 +473,10 @@ class S3Logger(CustomBatchLogger, BaseAWSLLM): if user_api_key_alias: prefix_components.append(user_api_key_alias) - # Construct full prefix path + # Construct full prefix path (sanitize characters invalid in S3 keys) prefix_path = "/".join(prefix_components) + # Replace spaces and other problematic characters to prevent S3 signing errors + prefix_path = re.sub(r"[\s]+", "_", prefix_path) if prefix_path: prefix_path += "/" diff --git a/tests/logging_callback_tests/test_s3_space_sanitize.py b/tests/logging_callback_tests/test_s3_space_sanitize.py new file mode 100644 index 00000000000..a98bb253841 --- /dev/null +++ b/tests/logging_callback_tests/test_s3_space_sanitize.py @@ -0,0 +1,44 @@ +""" +Test that S3 prefix path sanitizes spaces in team_alias and key_alias. + +Fixes: https://github.com/BerriAI/litellm/issues/25019 +""" + +import re + + +def _sanitize_prefix(prefix_path: str) -> str: + """Mirror the sanitization logic from s3_v2.py""" + return re.sub(r"[\s]+", "_", prefix_path) + + +def test_team_alias_with_spaces_sanitized(): + """Team names with spaces should have spaces replaced by underscores.""" + prefix_components = ["Cloud Tooling", "ugie-dev"] + prefix_path = "/".join(prefix_components) + prefix_path = _sanitize_prefix(prefix_path) + assert prefix_path == "Cloud_Tooling/ugie-dev" + + +def test_team_alias_with_multiple_spaces(): + """Multiple consecutive spaces collapse to a single underscore.""" + prefix_components = ["My Great Team"] + prefix_path = "/".join(prefix_components) + prefix_path = _sanitize_prefix(prefix_path) + assert prefix_path == "My_Great_Team" + + +def test_team_alias_no_spaces_unchanged(): + """Team names without spaces pass through unchanged.""" + prefix_components = ["engineering", "prod-key"] + prefix_path = "/".join(prefix_components) + prefix_path = _sanitize_prefix(prefix_path) + assert prefix_path == "engineering/prod-key" + + +def test_team_alias_with_tabs_and_newlines(): + """Tabs and newlines are also replaced.""" + prefix_components = ["team\twith\ttabs"] + prefix_path = "/".join(prefix_components) + prefix_path = _sanitize_prefix(prefix_path) + assert prefix_path == "team_with_tabs"