mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-14 09:03:16 +00:00
fix(caching): treat whitespace-only replies as no output
This commit is contained in:
parent
ddf3a86aad
commit
d21e29981b
2 changed files with 54 additions and 6 deletions
|
|
@ -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]:
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue