mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-09 22:31:41 +00:00
fix(anthropic): stream refusals parked in provider_specific_fields
The first-delta guard read `delta.refusal` directly, while the translation three lines later goes through `openai_chat_refusal_text`, which also reads the `provider_specific_fields` LiteLLM parks unrecognized fields in. A provider that sends the refusal that way had its only refusal delta skipped as blank, so the client got `stop_reason: refusal` over an empty content array, which is the symptom this PR set out to fix
This commit is contained in:
parent
b21315fe75
commit
c09d34fc4b
2 changed files with 41 additions and 1 deletions
|
|
@ -1064,6 +1064,9 @@ class AnthropicStreamWrapper(AdapterCompletionStreamWrapper):
|
|||
@staticmethod
|
||||
def _is_blank_delta(chunk: "ModelResponseStream") -> bool:
|
||||
from litellm.llms.anthropic.common_utils import is_empty_unsigned_thinking_block
|
||||
from litellm.llms.anthropic.experimental_pass_through.messages.utils import (
|
||||
openai_chat_refusal_text,
|
||||
)
|
||||
|
||||
choice: Final = chunk.choices[0]
|
||||
if choice.finish_reason is not None:
|
||||
|
|
@ -1073,7 +1076,7 @@ class AnthropicStreamWrapper(AdapterCompletionStreamWrapper):
|
|||
return False
|
||||
if getattr(delta, "content", None):
|
||||
return False
|
||||
if getattr(delta, "refusal", None):
|
||||
if openai_chat_refusal_text(delta):
|
||||
return False
|
||||
if getattr(delta, "reasoning_content", None):
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -145,6 +145,43 @@ async def test_streaming_chat_refusal_emits_refusal_text_and_stop_details_async(
|
|||
assert message_delta["delta"]["stop_details"]["explanation"] == "I cannot fulfill this request."
|
||||
|
||||
|
||||
def test_streaming_chat_refusal_parked_in_provider_specific_fields_is_emitted():
|
||||
"""Providers that do not populate ``delta.refusal`` (Azure o-series among
|
||||
them) hand LiteLLM the refusal as an unrecognized field, which lands in
|
||||
``provider_specific_fields``. That first delta still has to stream as text,
|
||||
otherwise the client gets ``stop_reason: refusal`` over an empty content
|
||||
array and shows the user nothing.
|
||||
"""
|
||||
chunks = [
|
||||
_make_chunk(Delta(content=None, provider_specific_fields={"refusal": "I cannot fulfill this request."})),
|
||||
_make_chunk(Delta(content=None), finish_reason="stop"),
|
||||
]
|
||||
wrapper = AnthropicStreamWrapper(completion_stream=iter(chunks), model="openai-model")
|
||||
|
||||
events = _drain_sync(wrapper)
|
||||
|
||||
assert _text_deltas(events) == ["I cannot fulfill this request."]
|
||||
message_delta = next(event for event in events if event["type"] == "message_delta")
|
||||
assert message_delta["delta"]["stop_reason"] == "refusal"
|
||||
assert message_delta["delta"]["stop_details"]["explanation"] == "I cannot fulfill this request."
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_streaming_chat_refusal_parked_in_provider_specific_fields_is_emitted_async():
|
||||
chunks = [
|
||||
_make_chunk(Delta(content=None, provider_specific_fields={"refusal": "I cannot fulfill this request."})),
|
||||
_make_chunk(Delta(content=None), finish_reason="stop"),
|
||||
]
|
||||
wrapper = AnthropicStreamWrapper(completion_stream=_AsyncStream(chunks), model="openai-model")
|
||||
|
||||
events = await _drain_async(wrapper)
|
||||
|
||||
assert _text_deltas(events) == ["I cannot fulfill this request."]
|
||||
message_delta = next(event for event in events if event["type"] == "message_delta")
|
||||
assert message_delta["delta"]["stop_reason"] == "refusal"
|
||||
assert message_delta["delta"]["stop_details"]["explanation"] == "I cannot fulfill this request."
|
||||
|
||||
|
||||
def test_streaming_chat_combined_refusal_and_finish_reason_is_preserved():
|
||||
chunks = [
|
||||
_make_chunk(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue