This commit is contained in:
agustin18 2026-09-30 12:51:03 -03:00 • committed by GitHub
commit 7fcf92805b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 148 additions and 19 deletions

View file

@ -676,7 +676,6 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
from openai.types.responses import (
ResponseFunctionToolCall,
ResponseOutputMessage,
ResponseReasoningItem,
)
try:
@ -691,7 +690,7 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
choices: Final[list[Choices]] = []
index = 0
reasoning_content: str | None = None
pending_reasoning_item: _BuiltReasoningItem | None = None
pending_reasoning_items: Final[list[_BuiltReasoningItem]] = [] # mutable-ok: accumulator
# Collect all tool calls to put them in a single choice
# (Chat Completions API expects all tool calls in one message)
@ -699,13 +698,13 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
tool_call_index = 0
for item in output_items:
if isinstance(item, ResponseReasoningItem):
pending_reasoning_item = _build_reasoning_item(
item_id=item.id,
encrypted_content=getattr(item, "encrypted_content", None),
summary_raw=item.summary,
)
reasoning_content = " ".join(s["text"] for s in pending_reasoning_item["summary"] if s.get("text"))
reasoning_item = _reasoning_item_from_output_item(item)
if reasoning_item is not None:
pending_reasoning_items.append(reasoning_item)
summary_texts = [s["text"] for s in reasoning_item["summary"] if s.get("text")]
if summary_texts:
step_text = " ".join(summary_texts)
reasoning_content = f"{reasoning_content} {step_text}".strip() if reasoning_content else step_text
elif isinstance(item, ResponseOutputMessage):
for content in item.content:
@ -720,10 +719,7 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
content=response_text if response_text else "",
reasoning_content=reasoning_content,
annotations=annotations,
reasoning_items=cast(
list[ChatCompletionReasoningItem] | None,
([pending_reasoning_item] if pending_reasoning_item is not None else None),
),
reasoning_items=_as_chat_reasoning_items(pending_reasoning_items),
)
choices.append(
@ -735,7 +731,7 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
)
reasoning_content = None # flush
pending_reasoning_item = None # flush
pending_reasoning_items.clear() # flush
index += 1
elif isinstance(item, ResponseFunctionToolCall):
@ -780,6 +776,12 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
elif handle_raw_dict_callback is not None:
choice, index = handle_raw_dict_callback(item=raw_item, index=index)
if choice is not None:
if pending_reasoning_items:
choice.message.reasoning_items = _as_chat_reasoning_items(pending_reasoning_items)
if reasoning_content:
choice.message.reasoning_content = reasoning_content
pending_reasoning_items.clear()
reasoning_content = None
choices.append(choice)
else:
pass # don't fail request if item in list is not supported
@ -790,14 +792,11 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
content=None,
tool_calls=accumulated_tool_calls,
reasoning_content=reasoning_content,
reasoning_items=cast(
list[ChatCompletionReasoningItem] | None,
([pending_reasoning_item] if pending_reasoning_item is not None else None),
),
reasoning_items=_as_chat_reasoning_items(pending_reasoning_items),
)
choices.append(Choices(message=msg, finish_reason="tool_calls", index=index))
reasoning_content = None
pending_reasoning_item = None
pending_reasoning_items.clear()
return choices

View file

@ -4388,3 +4388,133 @@ def test_every_bridged_chunk_after_response_created_carries_the_served_service_t
relayed = [iterator.chunk_parser(event).model_dump().get("service_tier") for event in events]
assert relayed == ["default"] * len(events), relayed
@pytest.mark.parametrize(
"raw_output_items, expected_reasoning_ids, expected_summary_text, target_type",
[
pytest.param(
[
{
"type": "reasoning",
"id": "rs_1",
"summary": [{"type": "summary_text", "text": "Step 1"}],
"encrypted_content": "enc1",
},
{
"type": "reasoning",
"id": "rs_2",
"summary": [{"type": "summary_text", "text": "Step 2"}],
"encrypted_content": "enc2",
},
{
"type": "function_call",
"id": "fc_1",
"call_id": "call_1",
"name": "search",
"arguments": "{}",
"status": "completed",
},
],
["rs_1", "rs_2"],
"Step 1 Step 2",
"tool_calls",
id="multiple_reasoning_items_before_tool_call",
),
pytest.param(
[
{
"type": "reasoning",
"id": "rs_alpha",
"summary": [{"type": "summary_text", "text": "Analysis"}],
"encrypted_content": "enc_a",
},
{
"type": "reasoning",
"id": "rs_beta",
"summary": [{"type": "summary_text", "text": "Conclusion"}],
"encrypted_content": "enc_b",
},
],
["rs_alpha", "rs_beta"],
"Analysis Conclusion",
"message",
id="multiple_reasoning_items_before_output_message",
),
pytest.param(
[
{
"type": "reasoning",
"id": "rs_raw_1",
"summary": [{"type": "summary_text", "text": "Raw Step 1"}],
"encrypted_content": "enc_raw",
},
{
"type": "message",
"role": "assistant",
"content": [{"type": "output_text", "text": "Raw response text"}],
},
],
["rs_raw_1"],
"Raw Step 1",
"raw_dict_message",
id="reasoning_items_before_raw_dict_message",
),
pytest.param(
[
{
"type": "function_call",
"id": "fc_solo",
"call_id": "call_solo",
"name": "lookup",
"arguments": "{}",
"status": "completed",
}
],
None,
None,
"tool_calls",
id="zero_reasoning_items_returns_none",
),
],
)
def test_convert_response_output_to_choices_preserves_all_reasoning_items(
raw_output_items: list[dict[str, object]],
expected_reasoning_ids: list[str] | None,
expected_summary_text: str | None,
target_type: str,
) -> None:
"""Verify non-streaming Responses API bridge retains all reasoning items across message and tool call turns."""
from openai.types.responses import ResponseOutputMessage
from openai.types.responses.response_output_message import ResponseOutputText
items_to_pass: Final[list[object]] = list(raw_output_items) # mutable-ok: fixture setup
if target_type == "message":
items_to_pass.append(
ResponseOutputMessage(
id="msg_1",
role="assistant",
status="completed",
type="message",
content=[ResponseOutputText(annotations=[], text="Done", type="output_text", logprobs=[])],
)
)
handler: Final = LiteLLMResponsesTransformationHandler()
choices: Final = handler._convert_response_output_to_choices(
items_to_pass,
handle_raw_dict_callback=handler._handle_raw_dict_response_item,
)
assert len(choices) == 1
msg = choices[0].message
reasoning_items = getattr(msg, "reasoning_items", None)
reasoning_content = getattr(msg, "reasoning_content", None)
if expected_reasoning_ids is None:
assert reasoning_items is None
assert reasoning_content is None
else:
assert reasoning_items is not None
assert [item["id"] for item in reasoning_items] == expected_reasoning_ids
assert reasoning_content == expected_summary_text