From cac3d2f16b91a6986411a66db63992c224b2db3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?d=20=F0=9F=94=B9?= Date: Fri, 3 Apr 2026 18:08:39 +0800 Subject: [PATCH 1/2] fix(s3): sanitize spaces in team_alias prefix to prevent 403 errors --- litellm/integrations/s3_v2.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 += "/" From bc72afac8ef9ac10876b1fafa9079a696d53fe74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?d=20=F0=9F=94=B9?= Date: Fri, 3 Apr 2026 18:08:45 +0800 Subject: [PATCH 2/2] test: add tests for S3 prefix space sanitization --- .../test_s3_space_sanitize.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 tests/logging_callback_tests/test_s3_space_sanitize.py 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"