mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
fix(adapter): handle GenericResponseOutputItem in translate_response
When use_chat_completions_api: true bridges to chat completions, the response output contains GenericResponseOutputItem (Pydantic models) instead of OpenAI SDK native types. translate_response() had no branch for these — the isinstance(item, dict) fallback doesn't match because Pydantic v2 BaseModel subclasses are not dicts. Content was silently dropped for any model using the chat completions bridge. Add an isinstance(item, GenericResponseOutputItem) branch handling both 'message' (-> text block) and 'reasoning' (-> thinking block) types. Tests: 3 new tests using real Pydantic GenericResponseOutputItem instances. Existing tests used MagicMock which passes isinstance() for any type.
This commit is contained in:
parent
fa81017e12
commit
16f4648269
2 changed files with 145 additions and 1 deletions
|
|
@ -412,7 +412,7 @@ class LiteLLMAnthropicToResponsesAPIAdapter:
|
|||
# Response translation: Responses API -> Anthropic #
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def translate_response(
|
||||
def translate_response( # noqa: PLR0915
|
||||
self,
|
||||
response: ResponsesAPIResponse,
|
||||
) -> AnthropicMessagesResponse:
|
||||
|
|
@ -426,6 +426,10 @@ class LiteLLMAnthropicToResponsesAPIAdapter:
|
|||
)
|
||||
|
||||
from litellm.types.llms.openai import ResponseAPIUsage
|
||||
from litellm.types.responses.main import (
|
||||
GenericResponseOutputItem,
|
||||
OutputText,
|
||||
)
|
||||
|
||||
content: List[Dict[str, Any]] = []
|
||||
stop_reason: AnthropicFinishReason = "end_turn"
|
||||
|
|
@ -467,6 +471,40 @@ class LiteLLMAnthropicToResponsesAPIAdapter:
|
|||
)
|
||||
stop_reason = "tool_use"
|
||||
|
||||
elif isinstance(item, GenericResponseOutputItem):
|
||||
if item.type == "reasoning":
|
||||
for part in (item.content or []):
|
||||
if isinstance(part, OutputText) and part.text:
|
||||
content.append(
|
||||
AnthropicResponseContentBlockThinking(
|
||||
type="thinking",
|
||||
thinking=part.text,
|
||||
signature=None,
|
||||
).model_dump()
|
||||
)
|
||||
elif item.type == "message":
|
||||
for part in (item.content or []):
|
||||
if isinstance(part, OutputText) and part.text:
|
||||
content.append(
|
||||
AnthropicResponseContentBlockText(
|
||||
type="text", text=part.text
|
||||
).model_dump()
|
||||
)
|
||||
elif item.type == "function_call":
|
||||
try:
|
||||
input_data = json.loads(item.arguments) if hasattr(item, "arguments") and item.arguments else {}
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
input_data = {}
|
||||
content.append(
|
||||
AnthropicResponseContentBlockToolUse(
|
||||
type="tool_use",
|
||||
id=getattr(item, "call_id", "") or getattr(item, "id", ""),
|
||||
name=getattr(item, "name", ""),
|
||||
input=input_data,
|
||||
).model_dump()
|
||||
)
|
||||
stop_reason = "tool_use"
|
||||
|
||||
elif isinstance(item, dict):
|
||||
item_type = item.get("type")
|
||||
if item_type == "message":
|
||||
|
|
|
|||
|
|
@ -9,6 +9,11 @@ import sys
|
|||
from typing import Any, Dict, List
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from litellm.types.responses.main import (
|
||||
GenericResponseOutputItem,
|
||||
OutputText,
|
||||
)
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../../../../../../.."))
|
||||
|
||||
from litellm.llms.anthropic.experimental_pass_through.responses_adapters.transformation import (
|
||||
|
|
@ -1043,3 +1048,104 @@ class TestTranslateResponse:
|
|||
assert "text" in types
|
||||
assert "tool_use" in types
|
||||
assert result["stop_reason"] == "tool_use"
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Real GenericResponseOutputItem (Pydantic) tests #
|
||||
# ------------------------------------------------------------------ #
|
||||
# These exercise the path taken when use_chat_completions_api: true
|
||||
# bridges to chat completions. The chat-completion bridge produces
|
||||
# GenericResponseOutputItem Pydantic instances, NOT OpenAI SDK types
|
||||
# or plain dicts. MagicMock passes isinstance(item, X) for any X,
|
||||
# so mock-only tests could never catch the Pydantic-vs-dict mismatch.
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def _make_real_response(self, output: list) -> Any:
|
||||
"""Build a real ResponsesAPIResponse with the given output items."""
|
||||
from litellm.types.llms.openai import ResponsesAPIResponse
|
||||
|
||||
return ResponsesAPIResponse(
|
||||
id="resp_real",
|
||||
output=output,
|
||||
created_at=0,
|
||||
model="test",
|
||||
object="response",
|
||||
status="completed",
|
||||
)
|
||||
|
||||
def test_generic_output_item_message_pydantic(self):
|
||||
"""GenericResponseOutputItem (type=message) Pydantic -> text block."""
|
||||
item = GenericResponseOutputItem(
|
||||
type="message",
|
||||
id="msg_1",
|
||||
status="completed",
|
||||
role="assistant",
|
||||
content=[
|
||||
OutputText(
|
||||
type="output_text",
|
||||
text="Hello from Pydantic!",
|
||||
annotations=[],
|
||||
)
|
||||
],
|
||||
)
|
||||
response = self._make_real_response(output=[item])
|
||||
result: Any = _ADAPTER.translate_response(response)
|
||||
assert len(result["content"]) == 1
|
||||
assert result["content"][0]["type"] == "text"
|
||||
assert result["content"][0]["text"] == "Hello from Pydantic!"
|
||||
|
||||
def test_generic_output_item_reasoning_pydantic(self):
|
||||
"""GenericResponseOutputItem (type=reasoning) Pydantic -> thinking block."""
|
||||
item = GenericResponseOutputItem(
|
||||
type="reasoning",
|
||||
id="rs_1",
|
||||
status="completed",
|
||||
role="assistant",
|
||||
content=[
|
||||
OutputText(
|
||||
type="output_text",
|
||||
text="I need to think about this first.",
|
||||
annotations=[],
|
||||
)
|
||||
],
|
||||
)
|
||||
response = self._make_real_response(output=[item])
|
||||
result: Any = _ADAPTER.translate_response(response)
|
||||
assert len(result["content"]) == 1
|
||||
assert result["content"][0]["type"] == "thinking"
|
||||
assert "think" in result["content"][0]["thinking"]
|
||||
|
||||
def test_generic_output_item_reasoning_plus_message_pydantic(self):
|
||||
"""Reasoning + message GenericResponseOutputItem -> thinking + text."""
|
||||
reasoning = GenericResponseOutputItem(
|
||||
type="reasoning",
|
||||
id="rs_1",
|
||||
status="completed",
|
||||
role="assistant",
|
||||
content=[OutputText(
|
||||
type="output_text",
|
||||
text="Let me reason step by step.",
|
||||
annotations=[],
|
||||
)],
|
||||
)
|
||||
message = GenericResponseOutputItem(
|
||||
type="message",
|
||||
id="msg_1",
|
||||
status="completed",
|
||||
role="assistant",
|
||||
content=[OutputText(
|
||||
type="output_text",
|
||||
text="The answer is 42.",
|
||||
annotations=[],
|
||||
)],
|
||||
)
|
||||
response = self._make_real_response(output=[reasoning, message])
|
||||
result: Any = _ADAPTER.translate_response(response)
|
||||
types = [b["type"] for b in result["content"]]
|
||||
assert "thinking" in types
|
||||
assert "text" in types
|
||||
texts = {
|
||||
b["type"]: b.get("text") or b.get("thinking", "")
|
||||
for b in result["content"]
|
||||
}
|
||||
assert "Let me reason" in texts["thinking"]
|
||||
assert "answer is 42" in texts["text"]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue