mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-14 23:21:35 +00:00
Merge d184087e6e into 9071ca503e
This commit is contained in:
commit
a01c2d98d7
2 changed files with 183 additions and 1 deletions
|
|
@ -393,7 +393,8 @@ class GenAIHubOrchestrationConfig(OpenAIGPTConfig):
|
|||
original_response=raw_response.text,
|
||||
additional_args={"complete_input_dict": request_data},
|
||||
)
|
||||
response = ModelResponse.model_validate(raw_response.json()["final_result"])
|
||||
final_result = self._normalize_reasoning_content(raw_response.json()["final_result"])
|
||||
response = ModelResponse.model_validate(final_result)
|
||||
|
||||
# Strip markdown code blocks if JSON response_format was used with Anthropic models
|
||||
# SAP GenAI Hub with Anthropic models sometimes wraps JSON in ```json ... ```
|
||||
|
|
@ -406,6 +407,42 @@ class GenAIHubOrchestrationConfig(OpenAIGPTConfig):
|
|||
|
||||
return response
|
||||
|
||||
@staticmethod
|
||||
def _normalize_reasoning_content(raw: dict[str, object]) -> dict[str, object]: # mutable-ok: generic types
|
||||
"""Normalize list-shaped reasoning_content to the string field litellm expects.
|
||||
|
||||
SAP AI Core forwards reasoning tokens from Gemini and other thinking models as:
|
||||
message.reasoning_content = [{"content": "...", "signature": "..."}, ...]
|
||||
|
||||
ModelResponse.reasoning_content is typed Optional[str], so model_validate
|
||||
raises a ValidationError on a list. Map the blocks to thinking_blocks
|
||||
(already typed for this shape) and set reasoning_content to the concatenated
|
||||
text so callers that read the string field still get something useful.
|
||||
"""
|
||||
new_choices = []
|
||||
for choice in raw.get("choices", []): # mutable-ok: sentinel default, never mutated
|
||||
msg = choice.get("message", {})
|
||||
rc = msg.get("reasoning_content")
|
||||
if not isinstance(rc, list):
|
||||
new_choices.append(choice)
|
||||
continue
|
||||
thinking_blocks = [ # mutable-ok: local accumulator built once and assigned
|
||||
{ # mutable-ok: each block dict constructed fresh per item
|
||||
"type": "thinking",
|
||||
"thinking": item.get("content") or "",
|
||||
"signature": item.get("signature"),
|
||||
}
|
||||
for item in rc
|
||||
if isinstance(item, dict)
|
||||
]
|
||||
new_msg = {
|
||||
**msg,
|
||||
"thinking_blocks": thinking_blocks,
|
||||
"reasoning_content": ("\n".join(b["thinking"] for b in thinking_blocks if b["thinking"]) or None),
|
||||
}
|
||||
new_choices.append({**choice, "message": new_msg})
|
||||
return {**raw, "choices": new_choices}
|
||||
|
||||
def _strip_markdown_json(self, response: ModelResponse) -> ModelResponse:
|
||||
"""Strip markdown code block wrapper from JSON content if present.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import warnings
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
|
|
@ -639,3 +640,147 @@ class TestSAPTransformationIntegration:
|
|||
config["config"]["modules"][1]["translation"]["input"]["type"]
|
||||
== "sap_document_translation"
|
||||
)
|
||||
|
||||
|
||||
class TestNormalizeReasoningContent:
|
||||
"""Unit tests for GenAIHubOrchestrationConfig._normalize_reasoning_content."""
|
||||
|
||||
from litellm.llms.sap.chat.transformation import GenAIHubOrchestrationConfig
|
||||
|
||||
_normalize = staticmethod(GenAIHubOrchestrationConfig._normalize_reasoning_content)
|
||||
|
||||
def test_list_reasoning_content_mapped_to_thinking_blocks(self):
|
||||
"""List-shaped reasoning_content is converted to thinking_blocks."""
|
||||
raw = {
|
||||
"choices": [{
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "Latin.",
|
||||
"reasoning_content": [
|
||||
{"content": "Romans spoke Latin.", "signature": "sig1"},
|
||||
{"content": "That is well known.", "signature": "sig2"},
|
||||
],
|
||||
}
|
||||
}]
|
||||
}
|
||||
out = self._normalize(raw)
|
||||
msg = out["choices"][0]["message"]
|
||||
assert msg["thinking_blocks"] == [
|
||||
{"type": "thinking", "thinking": "Romans spoke Latin.", "signature": "sig1"},
|
||||
{"type": "thinking", "thinking": "That is well known.", "signature": "sig2"},
|
||||
]
|
||||
assert msg["reasoning_content"] == "Romans spoke Latin.\nThat is well known."
|
||||
|
||||
def test_string_reasoning_content_unchanged(self):
|
||||
"""String reasoning_content is left as-is (already the right type)."""
|
||||
raw = {
|
||||
"choices": [{
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "42",
|
||||
"reasoning_content": "I thought about it.",
|
||||
}
|
||||
}]
|
||||
}
|
||||
out = self._normalize(raw)
|
||||
msg = out["choices"][0]["message"]
|
||||
assert msg["reasoning_content"] == "I thought about it."
|
||||
assert "thinking_blocks" not in msg
|
||||
|
||||
def test_no_reasoning_content_unchanged(self):
|
||||
"""A message without reasoning_content is not modified."""
|
||||
raw = {"choices": [{"message": {"role": "assistant", "content": "Hi."}}]}
|
||||
out = self._normalize(raw)
|
||||
assert out == raw
|
||||
|
||||
def test_empty_list_reasoning_content_sets_none(self):
|
||||
"""An empty list produces None for reasoning_content and empty thinking_blocks."""
|
||||
raw = {"choices": [{"message": {"reasoning_content": []}}]}
|
||||
out = self._normalize(raw)
|
||||
msg = out["choices"][0]["message"]
|
||||
assert msg["thinking_blocks"] == []
|
||||
assert msg["reasoning_content"] is None
|
||||
|
||||
def test_multiple_choices_all_normalized(self):
|
||||
"""All choices in the response are normalized."""
|
||||
raw = {
|
||||
"choices": [
|
||||
{"message": {"reasoning_content": [{"content": "thought A", "signature": None}]}},
|
||||
{"message": {"reasoning_content": [{"content": "thought B", "signature": "s"}]}},
|
||||
]
|
||||
}
|
||||
out = self._normalize(raw)
|
||||
assert out["choices"][0]["message"]["reasoning_content"] == "thought A"
|
||||
assert out["choices"][1]["message"]["reasoning_content"] == "thought B"
|
||||
|
||||
|
||||
def test_null_content_in_block_uses_empty_string(self):
|
||||
"""Explicit null content value must not leak None into thinking field."""
|
||||
raw = {
|
||||
"choices": [{
|
||||
"message": {
|
||||
"reasoning_content": [{"content": None, "signature": "s"}],
|
||||
}
|
||||
}]
|
||||
}
|
||||
out = self._normalize(raw)
|
||||
block = out["choices"][0]["message"]["thinking_blocks"][0]
|
||||
assert block["thinking"] == ""
|
||||
assert out["choices"][0]["message"]["reasoning_content"] is None
|
||||
|
||||
def test_transform_response_normalizes_list_reasoning_content(self):
|
||||
"""Production path: transform_response must produce a ModelResponse
|
||||
with thinking_blocks populated when the raw payload carries a
|
||||
list-shaped reasoning_content.
|
||||
"""
|
||||
import json
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from litellm.llms.sap.chat.transformation import GenAIHubOrchestrationConfig
|
||||
|
||||
config = GenAIHubOrchestrationConfig()
|
||||
|
||||
final_result = {
|
||||
"id": "chatcmpl-test",
|
||||
"object": "chat.completion",
|
||||
"created": 1700000000,
|
||||
"model": "gemini-test",
|
||||
"choices": [{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "The answer is 42.",
|
||||
"reasoning_content": [
|
||||
{"content": "Let me think.", "signature": "sig1"},
|
||||
{"content": "Yes, 42.", "signature": "sig2"},
|
||||
],
|
||||
},
|
||||
"finish_reason": "stop",
|
||||
}],
|
||||
"usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15},
|
||||
}
|
||||
|
||||
raw_response = MagicMock()
|
||||
raw_response.text = json.dumps({"final_result": final_result})
|
||||
raw_response.json.return_value = {"final_result": final_result}
|
||||
|
||||
response = config.transform_response(
|
||||
model="gemini-test",
|
||||
raw_response=raw_response,
|
||||
model_response=MagicMock(),
|
||||
logging_obj=MagicMock(),
|
||||
api_key="test",
|
||||
request_data={},
|
||||
messages=[],
|
||||
optional_params={},
|
||||
litellm_params={},
|
||||
encoding=None,
|
||||
)
|
||||
|
||||
choice = response.choices[0]
|
||||
assert hasattr(choice.message, "thinking_blocks"), "thinking_blocks missing from message"
|
||||
assert choice.message.thinking_blocks == [
|
||||
{"type": "thinking", "thinking": "Let me think.", "signature": "sig1"},
|
||||
{"type": "thinking", "thinking": "Yes, 42.", "signature": "sig2"},
|
||||
]
|
||||
assert choice.message.reasoning_content == "Let me think.\nYes, 42."
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue