mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-29 06:33:28 +00:00
fix(sap): denormalization added
This commit is contained in:
parent
247aa59f16
commit
b88c8eafc3
3 changed files with 109 additions and 13 deletions
|
|
@ -32,16 +32,6 @@ def _now_ts() -> int:
|
|||
return int(time.time())
|
||||
|
||||
|
||||
def normalize_reasoning_content(raw: dict[str, object]) -> dict[str, object]:
|
||||
"""Transform reasoning_content from SAP Orchestration format [{"content": ..., "signature": ...}] to Optional[str] and a thinking_blocks list."""
|
||||
choices: Final[list[object]] = list(raw.get("choices") or [])
|
||||
normalized_choices: Final[dict[str, object]] = [
|
||||
normalize_choice(choice) for choice in choices if isinstance(choice, dict)
|
||||
]
|
||||
normalized_result: Final[dict[str, object]] = {**raw, "choices": normalized_choices}
|
||||
return normalized_result
|
||||
|
||||
|
||||
def normalize_choice(choice: dict[str, object]) -> dict[str, object]:
|
||||
"""Normalize a single choice dict (message or delta shape) in isolation."""
|
||||
for key in ("message", "delta"):
|
||||
|
|
@ -67,6 +57,40 @@ def normalize_choice(choice: dict[str, object]) -> dict[str, object]:
|
|||
return choice
|
||||
|
||||
|
||||
def normalize_reasoning_content(raw: dict[str, object]) -> dict[str, object]:
|
||||
"""Transform reasoning_content from SAP Orchestration format [{"content": ..., "signature": ...}] to Optional[str] and a thinking_blocks list."""
|
||||
choices: Final[list[object]] = list(raw.get("choices") or [])
|
||||
normalized_choices: Final[dict[str, object]] = [
|
||||
normalize_choice(choice) for choice in choices if isinstance(choice, dict)
|
||||
]
|
||||
normalized_result: Final[dict[str, object]] = {**raw, "choices": normalized_choices}
|
||||
return normalized_result
|
||||
|
||||
|
||||
def denormalize_reasoning_content(message: dict[str, object]) -> dict[str, object]:
|
||||
"""Inverse of normalize_reasoning_content: rebuild list[ReasoningBlock] from thinking_blocks.
|
||||
|
||||
LiteLLM normalizes SAP's reasoning_content list into a str + thinking_blocks before
|
||||
returning a ModelResponse. When that response is replayed as a history message the dict
|
||||
must be converted back to the SAP shape before SAPAssistantMessage validation.
|
||||
"""
|
||||
if message.get("role") != "assistant":
|
||||
return message
|
||||
thinking_blocks = message.get("thinking_blocks")
|
||||
if not isinstance(thinking_blocks, list):
|
||||
return message
|
||||
reasoning_content: Final = [
|
||||
{"content": block.get("thinking") or "", "signature": block.get("signature")}
|
||||
for block in thinking_blocks
|
||||
if isinstance(block, dict)
|
||||
]
|
||||
return {
|
||||
k: v
|
||||
for k, v in message.items()
|
||||
if k not in ("thinking_blocks", "reasoning_content")
|
||||
} | {"reasoning_content": reasoning_content}
|
||||
|
||||
|
||||
def _is_terminal_chunk(chunk: OpenAIChatCompletionChunk) -> bool:
|
||||
"""OpenAI-shaped chunk is terminal if any choice has a non-None finish_reason."""
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ from .handler import (
|
|||
GenAIHubOrchestrationError,
|
||||
SAPStreamIterator,
|
||||
normalize_reasoning_content,
|
||||
denormalize_reasoning_content,
|
||||
)
|
||||
from .models import (
|
||||
ChatCompletionTool,
|
||||
|
|
@ -104,8 +105,9 @@ def _messages_to_sap_template(messages: list[AllMessageValues]) -> list:
|
|||
template: Final = []
|
||||
for message in messages:
|
||||
folded_message = _fold_message_cache_control(message)
|
||||
message_role_class = _message_role_mapping(str(folded_message["role"]))
|
||||
validated_message = validate_dict(folded_message, message_role_class)
|
||||
denormalized_message = denormalize_reasoning_content(folded_message)
|
||||
message_role_class = _message_role_mapping(str(denormalized_message["role"]))
|
||||
validated_message = validate_dict(denormalized_message, message_role_class)
|
||||
template.append(validated_message)
|
||||
return template
|
||||
|
||||
|
|
|
|||
|
|
@ -1141,4 +1141,74 @@ class TestMessagesToSapTemplateReasoningContent:
|
|||
assistant_msg = result[1]
|
||||
assert assistant_msg["reasoning_content"] == [
|
||||
{"content": "Simple arithmetic.", "signature": "sig1"}
|
||||
]
|
||||
]
|
||||
|
||||
def test_litellm_normalized_assistant_message_round_trips_signature(self):
|
||||
"""After a non-streaming SAP call, the ModelResponse has reasoning_content: str
|
||||
and thinking_blocks: list (LiteLLM native format). Replaying it as a history
|
||||
message must not raise and must preserve the signature.
|
||||
"""
|
||||
from litellm.llms.sap.chat.transformation import _messages_to_sap_template
|
||||
|
||||
signature = "EtwHCkABGAIqQKNbQn0Uz=="
|
||||
messages = [
|
||||
{"role": "user", "content": "Translate it."},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "The text is Lorem Ipsum.",
|
||||
"reasoning_content": "Lorem ipsum is pseudo-Latin...",
|
||||
"thinking_blocks": [
|
||||
{"type": "thinking", "thinking": "Lorem ipsum is pseudo-Latin...", "signature": signature}
|
||||
],
|
||||
},
|
||||
{"role": "user", "content": "Summarise it."},
|
||||
]
|
||||
|
||||
result = _messages_to_sap_template(messages)
|
||||
assistant = next(m for m in result if m.get("role") == "assistant")
|
||||
|
||||
reasoning = assistant.get("reasoning_content")
|
||||
assert isinstance(reasoning, list)
|
||||
assert any(block.get("signature") == signature for block in reasoning)
|
||||
|
||||
def test_streaming_assembled_assistant_message_round_trips_signature(self):
|
||||
"""After a streaming SAP call, normalize_reasoning_content runs on every chunk.
|
||||
The assembled assistant message has the same LiteLLM-native shape as the
|
||||
non-streaming case. Verify the signature survives the round-trip.
|
||||
"""
|
||||
from litellm.llms.sap.chat.handler import normalize_reasoning_content
|
||||
from litellm.llms.sap.chat.transformation import _messages_to_sap_template
|
||||
|
||||
signature = "EtwHCkABGAIqQKNbQn0Uz=="
|
||||
thinking_text = "Lorem ipsum is pseudo-Latin..."
|
||||
sap_final_result = {
|
||||
"id": "msg_stream_01",
|
||||
"object": "chat.completion",
|
||||
"model": "gpt-4o",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "The text is Lorem Ipsum.",
|
||||
"reasoning_content": [{"content": thinking_text, "signature": signature}],
|
||||
},
|
||||
"finish_reason": "stop",
|
||||
}
|
||||
],
|
||||
}
|
||||
normalized = normalize_reasoning_content(sap_final_result)
|
||||
assembled_message = {"role": "assistant", **normalized["choices"][0]["message"]}
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": "Translate it."},
|
||||
assembled_message,
|
||||
{"role": "user", "content": "Summarize it."},
|
||||
]
|
||||
|
||||
result = _messages_to_sap_template(messages)
|
||||
assistant = next(m for m in result if m.get("role") == "assistant")
|
||||
|
||||
reasoning = assistant.get("reasoning_content")
|
||||
assert isinstance(reasoning, list)
|
||||
assert any(block.get("signature") == signature for block in reasoning)
|
||||
Loading…
Add table
Reference in a new issue