Skip to content

Commit 2563c0a

Browse files
Merge pull request #14534 from hula-la/fix/response-api-bugfixes
fix: improve response api handling and cold storage configuration
2 parents fcf8402 + 1585f5b commit 2563c0a

File tree

4 files changed

+157
-3
lines changed

4 files changed

+157
-3
lines changed

litellm/litellm_core_utils/litellm_logging.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4150,15 +4150,28 @@ def _generate_cold_storage_object_key(
41504150
from litellm.integrations.s3 import get_s3_object_key
41514151

41524152
# Only generate object key if cold storage is configured
4153-
if litellm.configured_cold_storage_logger is None:
4153+
configured_cold_storage_logger = litellm.configured_cold_storage_logger
4154+
if configured_cold_storage_logger is None:
41544155
return None
41554156

41564157
try:
41574158
# Generate file name in same format as litellm.utils.get_logging_id
41584159
s3_file_name = f"time-{start_time.strftime('%H-%M-%S-%f')}_{response_id}"
41594160

4161+
# Get the actual s3_path from the configured cold storage logger instance
4162+
s3_path = "" # default value
4163+
4164+
# Try to get the actual logger instance from the logger name
4165+
try:
4166+
custom_logger = litellm.logging_callback_manager.get_active_custom_logger_for_callback_name(configured_cold_storage_logger)
4167+
if custom_logger and hasattr(custom_logger, 's3_path') and custom_logger.s3_path:
4168+
s3_path = custom_logger.s3_path
4169+
except Exception:
4170+
# If any error occurs in getting the logger instance, use default empty s3_path
4171+
pass
4172+
41604173
s3_object_key = get_s3_object_key(
4161-
s3_path="", # Use empty path as default
4174+
s3_path=s3_path, # Use actual s3_path from logger configuration
41624175
team_alias_prefix="", # Don't split by team alias for cold storage
41634176
start_time=start_time,
41644177
s3_file_name=s3_file_name,

litellm/responses/litellm_completion_transformation/session_handler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ async def extend_chat_completion_message_with_spend_log_payload(
141141
# Add Output messages for this Spend Log
142142
############################################################
143143
_response_output = spend_log.get("response", "{}")
144-
if isinstance(_response_output, dict):
144+
if isinstance(_response_output, dict) and _response_output and _response_output != {}:
145145
# transform `ChatCompletion Response` to `ResponsesAPIResponse`
146146
model_response = ModelResponse(**_response_output)
147147
for choice in model_response.choices:

tests/test_litellm/litellm_core_utils/test_litellm_logging.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,97 @@ async def test_e2e_generate_cold_storage_object_key_successful():
438438
assert isinstance(result, str)
439439

440440

441+
@pytest.mark.asyncio
442+
async def test_e2e_generate_cold_storage_object_key_with_custom_logger_s3_path():
443+
"""
444+
Test that _generate_cold_storage_object_key uses s3_path from custom logger instance.
445+
"""
446+
from datetime import datetime, timezone
447+
from unittest.mock import MagicMock, patch
448+
449+
from litellm.litellm_core_utils.litellm_logging import StandardLoggingPayloadSetup
450+
451+
# Create test data
452+
start_time = datetime(2025, 1, 15, 10, 30, 45, 123456, timezone.utc)
453+
response_id = "chatcmpl-test-12345"
454+
455+
# Create mock custom logger with s3_path
456+
mock_custom_logger = MagicMock()
457+
mock_custom_logger.s3_path = "storage"
458+
459+
with patch("litellm.configured_cold_storage_logger", "s3_v2"), \
460+
patch("litellm.logging_callback_manager.get_active_custom_logger_for_callback_name") as mock_get_logger, \
461+
patch("litellm.integrations.s3.get_s3_object_key") as mock_get_s3_key:
462+
463+
# Setup mocks
464+
mock_get_logger.return_value = mock_custom_logger
465+
mock_get_s3_key.return_value = "storage/2025-01-15/time-10-30-45-123456_chatcmpl-test-12345.json"
466+
467+
# Call the function
468+
result = StandardLoggingPayloadSetup._generate_cold_storage_object_key(
469+
start_time=start_time,
470+
response_id=response_id
471+
)
472+
473+
# Verify logger was queried correctly
474+
mock_get_logger.assert_called_once_with("s3_v2")
475+
476+
# Verify the S3 function was called with the custom logger's s3_path
477+
mock_get_s3_key.assert_called_once_with(
478+
s3_path="storage", # Should use custom logger's s3_path
479+
team_alias_prefix="",
480+
start_time=start_time,
481+
s3_file_name="time-10-30-45-123456_chatcmpl-test-12345"
482+
)
483+
484+
# Verify the result
485+
assert result == "storage/2025-01-15/time-10-30-45-123456_chatcmpl-test-12345.json"
486+
487+
488+
@pytest.mark.asyncio
489+
async def test_e2e_generate_cold_storage_object_key_with_logger_no_s3_path():
490+
"""
491+
Test that _generate_cold_storage_object_key falls back to empty s3_path when logger has no s3_path.
492+
"""
493+
from datetime import datetime, timezone
494+
from unittest.mock import MagicMock, patch
495+
496+
from litellm.litellm_core_utils.litellm_logging import StandardLoggingPayloadSetup
497+
498+
# Create test data
499+
start_time = datetime(2025, 1, 15, 10, 30, 45, 123456, timezone.utc)
500+
response_id = "chatcmpl-test-12345"
501+
502+
# Create mock custom logger without s3_path
503+
mock_custom_logger = MagicMock()
504+
mock_custom_logger.s3_path = None # or could be missing attribute
505+
506+
with patch("litellm.configured_cold_storage_logger", "s3_v2"), \
507+
patch("litellm.logging_callback_manager.get_active_custom_logger_for_callback_name") as mock_get_logger, \
508+
patch("litellm.integrations.s3.get_s3_object_key") as mock_get_s3_key:
509+
510+
# Setup mocks
511+
mock_get_logger.return_value = mock_custom_logger
512+
mock_get_s3_key.return_value = "2025-01-15/time-10-30-45-123456_chatcmpl-test-12345.json"
513+
514+
# Call the function
515+
result = StandardLoggingPayloadSetup._generate_cold_storage_object_key(
516+
start_time=start_time,
517+
response_id=response_id
518+
)
519+
520+
# Verify the S3 function was called with empty s3_path (fallback)
521+
mock_get_s3_key.assert_called_once_with(
522+
s3_path="", # Should fall back to empty string
523+
team_alias_prefix="",
524+
start_time=start_time,
525+
s3_file_name="time-10-30-45-123456_chatcmpl-test-12345"
526+
)
527+
528+
# Verify the result
529+
assert result == "2025-01-15/time-10-30-45-123456_chatcmpl-test-12345.json"
530+
531+
441532
@pytest.mark.asyncio
442533
async def test_e2e_generate_cold_storage_object_key_not_configured():
443534
"""

tests/test_litellm/responses/litellm_completion_transformation/test_session_handler.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,3 +364,53 @@ async def test_should_check_cold_storage_for_full_payload():
364364
with patch.object(litellm, 'configured_cold_storage_logger', None):
365365
result5 = ResponsesSessionHandler._should_check_cold_storage_for_full_payload(proxy_request_with_truncated_pdf)
366366
assert result5 == False, "Should return False when cold storage is not configured, even with truncated content"
367+
368+
369+
@pytest.mark.asyncio
370+
async def test_get_chat_completion_message_history_empty_response_dict():
371+
"""
372+
Test that empty response dict is handled correctly without processing.
373+
This tests the fix for response validation to check for empty dict responses.
374+
"""
375+
from unittest.mock import AsyncMock, patch
376+
377+
# Mock spend logs with empty response dict
378+
mock_spend_logs = [
379+
{
380+
"request_id": "chatcmpl-test-empty-response",
381+
"call_type": "aresponses",
382+
"api_key": "test_key",
383+
"spend": 0.001,
384+
"total_tokens": 0,
385+
"prompt_tokens": 0,
386+
"completion_tokens": 0,
387+
"startTime": "2025-01-15T10:30:00.000+00:00",
388+
"endTime": "2025-01-15T10:30:01.000+00:00",
389+
"model": "gpt-4",
390+
"session_id": "test-session",
391+
"proxy_server_request": {
392+
"input": "test input",
393+
"model": "gpt-4"
394+
},
395+
"response": {} # Empty dict - should not be processed
396+
}
397+
]
398+
399+
with patch.object(ResponsesSessionHandler, "get_all_spend_logs_for_previous_response_id") as mock_get_spend_logs:
400+
mock_get_spend_logs.return_value = mock_spend_logs
401+
402+
# Call the function
403+
result = await ResponsesSessionHandler.get_chat_completion_message_history_for_previous_response_id(
404+
"chatcmpl-test-empty-response"
405+
)
406+
407+
# Verify that user message was added but no assistant response
408+
# Since response is empty dict, no assistant response should be processed
409+
# But user input from proxy_server_request should still be included
410+
messages = result["messages"]
411+
assert len(messages) == 1 # Only user message, no assistant response
412+
assert messages[0]["role"] == "user"
413+
assert messages[0]["content"] == "test input"
414+
415+
# Verify the session was still created correctly
416+
assert result["litellm_session_id"] == "test-session"

0 commit comments

Comments
 (0)