diff --git a/litellm/caching/caching_handler.py b/litellm/caching/caching_handler.py index e924d829bfa..1d06b6804fc 100644 --- a/litellm/caching/caching_handler.py +++ b/litellm/caching/caching_handler.py @@ -154,9 +154,13 @@ def _content_block_payload(event: Mapping[str, object]) -> object: return None +def _is_blank(value: object) -> bool: + return value in ("", [], {}, None) or (isinstance(value, str) and not value.strip()) + + def _content_block_carries_output(block: object) -> bool: fields: Final = as_str_mapping(block) - return fields is not None and any(key != "type" and value not in ("", [], None) for key, value in fields.items()) + return fields is not None and any(key != "type" and not _is_blank(value) for key, value in fields.items()) def _sse_line_carries_output(line: str) -> bool: @@ -178,12 +182,9 @@ def _message_field_carries_output(key: str, value: object) -> bool: return False case "provider_specific_fields": fields: Final = as_str_mapping(value) or {} - return any( - name not in _REPLAY_METADATA_FIELDS and field not in ("", [], {}, None) - for name, field in fields.items() - ) + return any(name not in _REPLAY_METADATA_FIELDS and not _is_blank(field) for name, field in fields.items()) case _: - return value not in ("", [], {}, None) + return not _is_blank(value) def _choice_output_fields(choice: object) -> Mapping[str, object]: diff --git a/tests/unit/caching/test_caching_handler.py b/tests/unit/caching/test_caching_handler.py index 5479087013a..2b7d7fa0e14 100644 --- a/tests/unit/caching/test_caching_handler.py +++ b/tests/unit/caching/test_caching_handler.py @@ -2765,6 +2765,10 @@ def _anthropic_message_with_only_an_empty_text_block() -> dict[str, object]: } +def _anthropic_message_with_only_a_whitespace_text_block() -> dict[str, object]: + return {**_anthropic_message_with_only_an_empty_text_block(), "content": [{"type": "text", "text": "\n\n"}]} + + def _empty_reply_with_provider_metadata(provider_specific_fields: dict[str, object]) -> ModelResponse: return litellm.ModelResponse( choices=[litellm.Choices(message=litellm.Message(content="", provider_specific_fields=provider_specific_fields))] @@ -2846,6 +2850,18 @@ def _empty_response_cases() -> list[EmptyResponseCase]: CallTypes.aanthropic_messages.value, {"messages": _unique_messages(), "max_tokens": 16}, ), + ( + _anthropic_message_with_only_a_whitespace_text_block(), + aanthropic_messages, + CallTypes.aanthropic_messages.value, + {"messages": _unique_messages(), "max_tokens": 16}, + ), + ( + litellm.ModelResponse(choices=[litellm.Choices(message=litellm.Message(content="\n\n"))]), + litellm.acompletion, + CallTypes.acompletion.value, + {"messages": _unique_messages()}, + ), ( _empty_reply_with_provider_metadata({"thought_signatures": ["c2lnbmF0dXJl"]}), litellm.acompletion, @@ -2897,6 +2913,15 @@ def _empty_response_cases() -> list[EmptyResponseCase]: CallTypes.aanthropic_messages.value, {"messages": _unique_messages(), "max_tokens": 16, "stream": True}, ), + ( + _anthropic_stream_with_only_empty_blocks( + {"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}}, + {"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "\n\n"}}, + ), + aanthropic_messages, + CallTypes.aanthropic_messages.value, + {"messages": _unique_messages(), "max_tokens": 16, "stream": True}, + ), ] @@ -2951,6 +2976,28 @@ async def test_async_get_cache_treats_stored_response_without_output_as_miss( assert lookup.cached_result is None +@pytest.mark.asyncio +async def test_async_set_cache_keeps_reply_whose_text_is_padded_with_whitespace(): + setup_cache() + handler: Final = LLMCachingHandler(original_function=litellm.acompletion, request_kwargs={}, start_time=_FIXED_START) + kwargs: Final = {"messages": _unique_messages()} + reply: Final = litellm.ModelResponse(choices=[litellm.Choices(message=litellm.Message(content="\n\nParis\n"))]) + + await handler.async_set_cache(result=reply, original_function=litellm.acompletion, kwargs=kwargs) + await asyncio.gather(*_PENDING_CACHE_WRITES) + lookup: Final = await handler.async_get_cache( + model="gpt-3.5-turbo", + original_function=litellm.acompletion, + logging_obj=_completion_logging_obj(CallTypes.acompletion.value), + start_time=_FIXED_START, + call_type=CallTypes.acompletion.value, + kwargs=kwargs, + ) + + assert isinstance(lookup.cached_result, ModelResponse) + assert lookup.cached_result.choices[0].message.content == "\n\nParis\n" + + @pytest.mark.asyncio async def test_async_get_cache_heals_stored_completion_without_choices(): setup_cache()