mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-12 08:42:18 +00:00
fix(responses): leave a turn without a phase when its content message cannot be read
This commit is contained in:
parent
10adc6359c
commit
cd6923c5eb
2 changed files with 27 additions and 4 deletions
|
|
@ -51,6 +51,10 @@ _OutputItem: TypeAlias = Annotated[
|
|||
_OUTPUT_ITEM: Final[TypeAdapter[_OutputItem]] = TypeAdapter(_OutputItem)
|
||||
|
||||
|
||||
class _AnyMessage(_Frozen):
|
||||
type: Literal["message"]
|
||||
|
||||
|
||||
class _MessageEntry(_Frozen):
|
||||
type: Literal["message"]
|
||||
parts: tuple[Annotated[int, Field(ge=0)], ...]
|
||||
|
|
@ -155,6 +159,14 @@ def _parsed_output_item(item: object) -> _OutputItem | None:
|
|||
return None
|
||||
|
||||
|
||||
def _is_message(item: object) -> bool:
|
||||
try:
|
||||
_AnyMessage.model_validate(_as_json(item))
|
||||
except ValidationError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _chat_message(message: object) -> _ChatMessage | None:
|
||||
try:
|
||||
return _ChatMessage.model_validate(_as_json(message))
|
||||
|
|
@ -263,11 +275,14 @@ def _matches(items: Sequence[_OutputItem], message: _ChatMessage) -> bool:
|
|||
)
|
||||
|
||||
|
||||
def _content_phase(items: Sequence[_OutputItem | None], *, first_message_only: bool) -> Phase | None:
|
||||
messages: Final = tuple(item for item in items if isinstance(item, _OutputMessage))
|
||||
def _content_phase(
|
||||
output_items: Sequence[object], parsed: Sequence[_OutputItem | None], *, first_message_only: bool
|
||||
) -> Phase | None:
|
||||
messages: Final = tuple(item for raw, item in zip(output_items, parsed) if _is_message(raw))
|
||||
if not messages:
|
||||
return None
|
||||
return messages[0].phase if first_message_only else messages[-1].phase
|
||||
content_message: Final = messages[0] if first_message_only else messages[-1]
|
||||
return content_message.phase if isinstance(content_message, _OutputMessage) else None
|
||||
|
||||
|
||||
def _with_phase(message: _ChatMessage, phase: Phase | None) -> _ChatMessage:
|
||||
|
|
@ -281,7 +296,7 @@ def replay_fields(
|
|||
output_items: Sequence[object], message: object, *, completed: bool, first_message_only: bool
|
||||
) -> Mapping[str, object]:
|
||||
parsed: Final = tuple(map(_parsed_output_item, output_items))
|
||||
phase: Final = _content_phase(parsed, first_message_only=first_message_only)
|
||||
phase: Final = _content_phase(output_items, parsed, first_message_only=first_message_only)
|
||||
phase_fields: Final[Mapping[str, object]] = MappingProxyType({PHASE_FIELD: phase} if phase else {})
|
||||
items: Final = tuple(item for item in parsed if item is not None)
|
||||
parsed_message: Final = _chat_message(message)
|
||||
|
|
|
|||
|
|
@ -367,6 +367,14 @@ def test_a_lone_surrogate_in_the_reply_still_gets_a_record(stream: bool) -> None
|
|||
assert "openai_responses_replay" in _replayed_fields(assistant)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("stream", [False, True], ids=["json", "stream"])
|
||||
def test_a_last_message_the_bridge_cannot_read_leaves_the_turn_without_a_phase(stream: bool) -> None:
|
||||
unreadable: Final = {**_with_text(FINAL_ANSWER, "It is raining in Paris."), "phase": "summary"}
|
||||
assistant: Final = _assistant_turn((COMMENTARY, WEATHER_CALL, unreadable), stream=stream)
|
||||
|
||||
assert "phase" not in _replayed_fields(assistant)
|
||||
|
||||
|
||||
def test_a_phase_openai_does_not_define_is_dropped_under_drop_params() -> None:
|
||||
assistant: Final = _assistant_turn(OUTPUT, stream=False)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue