test(sap): consolidate regression tests and drop explanatory comments

Address Greptile review: move the stream-chunk normalization cases into
test_sap_chat_calls.py and the deployment_url 404 case into
test_sap_transformation.py, then delete the standalone module so bug-fix
regressions extend the mapped SAP test files. Strip the explanatory
docstring from _validate_chunk, keeping only the mutable-ok suppressions.
This commit is contained in:
ZOU Yi (BD/SWD-WDE1) 2026-09-07 14:20:16 +08:00
parent 77890478a1
commit 264d05b1fa
4 changed files with 125 additions and 146 deletions

View file

@ -51,15 +51,6 @@ class _StreamParser:
def _validate_chunk(
payload: dict, # mutable-ok: normalized in place (pops empty logprobs) before validation
) -> OpenAIChatCompletionChunk:
"""
Validate an OpenAI-shaped dict into a chunk, normalizing fields that would
otherwise break downstream serialization:
- drop the empty `logprobs` ({}) the orchestration service sends on every choice
- replace the raw openai-SDK usage object (deferred-build pydantic model whose
serializer is still a MockValSer) with litellm's Usage, so nested
model_dump() calls in the streaming handler don't raise
"'MockValSer' object is not an instance of 'SchemaSerializer'"
"""
for choice in payload.get("choices") or []: # mutable-ok: only iterated, never mutated
if isinstance(choice, dict) and not choice.get("logprobs"):
choice.pop("logprobs", None)

View file

@ -204,3 +204,113 @@ async def test_sap_chat_required_headers(
f"Header '{header_name}' has incorrect value. "
f"Expected: '{expected_value}', Got: '{request.headers[header_name]}'"
)
def _final_chunk_payload() -> dict:
return {
"id": "chatcmpl-sap-final",
"object": "chat.completion.chunk",
"created": 1761319270,
"model": "anthropic--claude-4.7-opus",
"choices": [
{
"index": 0,
"delta": {},
"logprobs": {},
"finish_reason": "tool_calls",
}
],
"usage": {
"completion_tokens": 206,
"prompt_tokens": 62322,
"total_tokens": 62528,
},
}
def test_validate_chunk_drops_empty_logprobs():
from litellm.llms.sap.chat.handler import _StreamParser
chunk = _StreamParser._validate_chunk(_final_chunk_payload())
assert chunk.choices[0].logprobs is None
def test_validate_chunk_preserves_real_logprobs():
from litellm.llms.sap.chat.handler import _StreamParser
payload = _final_chunk_payload()
payload["choices"][0]["logprobs"] = {
"content": [{"token": "Hello", "logprob": -0.1, "bytes": None, "top_logprobs": []}]
}
chunk = _StreamParser._validate_chunk(payload)
assert chunk.choices[0].logprobs is not None
assert chunk.choices[0].logprobs.content[0].token == "Hello"
def test_validate_chunk_converts_usage_to_litellm_usage():
from litellm.llms.sap.chat.handler import _StreamParser
from litellm.types.utils import Usage
chunk = _StreamParser._validate_chunk(_final_chunk_payload())
assert isinstance(chunk.usage, Usage)
assert chunk.usage.completion_tokens == 206
assert chunk.usage.prompt_tokens == 62322
assert chunk.usage.total_tokens == 62528
def test_validate_chunk_without_usage_keeps_none():
from litellm.llms.sap.chat.handler import _StreamParser
payload = _final_chunk_payload()
del payload["usage"]
chunk = _StreamParser._validate_chunk(payload)
assert chunk.usage is None
def test_validated_usage_survives_nested_model_dump():
from litellm.llms.sap.chat.handler import _StreamParser
from litellm.types.utils import ModelResponseStream
chunk = _StreamParser._validate_chunk(_final_chunk_payload())
model_response = ModelResponseStream()
setattr(model_response, "usage", chunk.usage)
dumped = model_response.model_dump()
assert dumped["usage"]["total_tokens"] == 62528
def test_to_openai_chunk_normalizes_openai_shaped_event():
from litellm.llms.sap.chat.handler import _StreamParser
from litellm.types.utils import Usage
chunk = _StreamParser.to_openai_chunk(_final_chunk_payload())
assert chunk is not None
assert chunk.choices[0].logprobs is None
assert isinstance(chunk.usage, Usage)
def test_to_openai_chunk_from_orchestration_result():
from litellm.llms.sap.chat.handler import _StreamParser
event = {
"request_id": "a07127d3-cb74-9427-a4dc-ef9bf424fb43",
"orchestration_result": {
"id": "chatcmpl-sap-delta",
"object": "chat.completion.chunk",
"created": 1761319270,
"model": "anthropic--claude-4.7-opus",
"choices": [
{
"index": 0,
"delta": {"role": "assistant", "content": "Hello "},
"logprobs": {},
"finish_reason": None,
}
],
},
}
chunk = _StreamParser.to_openai_chunk(event)
assert chunk is not None
assert chunk.choices[0].delta.content == "Hello "
assert chunk.choices[0].logprobs is None

View file

@ -1,137 +0,0 @@
"""
Tests for SAP orchestration stream chunk normalization (_StreamParser._validate_chunk)
and the descriptive error raised when no orchestration deployment exists.
Regression tests for:
- TypeError: 'MockValSer' object is not an instance of 'SchemaSerializer'
raised from nested model_dump() when the raw openai-SDK usage object
(a deferred-build pydantic model) was attached to ModelResponseStream.
- IndexError: list index out of range raised from deployment_url when the
configured resource group contains no orchestration deployment.
"""
from unittest.mock import MagicMock
import pytest
from litellm.llms.sap.chat.handler import GenAIHubOrchestrationError, _StreamParser
from litellm.types.utils import ModelResponseStream, Usage
def _final_chunk_payload() -> dict:
"""OpenAI-shaped final chunk as sent by the SAP orchestration service:
every choice carries an empty `logprobs` ({}) and the last chunk carries usage."""
return {
"id": "chatcmpl-sap-final",
"object": "chat.completion.chunk",
"created": 1761319270,
"model": "anthropic--claude-4.7-opus",
"choices": [
{
"index": 0,
"delta": {},
"logprobs": {},
"finish_reason": "tool_calls",
}
],
"usage": {
"completion_tokens": 206,
"prompt_tokens": 62322,
"total_tokens": 62528,
},
}
def test_validate_chunk_drops_empty_logprobs():
chunk = _StreamParser._validate_chunk(_final_chunk_payload())
assert chunk.choices[0].logprobs is None
def test_validate_chunk_preserves_real_logprobs():
payload = _final_chunk_payload()
payload["choices"][0]["logprobs"] = {
"content": [{"token": "Hello", "logprob": -0.1, "bytes": None, "top_logprobs": []}]
}
chunk = _StreamParser._validate_chunk(payload)
assert chunk.choices[0].logprobs is not None
assert chunk.choices[0].logprobs.content[0].token == "Hello"
def test_validate_chunk_converts_usage_to_litellm_usage():
chunk = _StreamParser._validate_chunk(_final_chunk_payload())
assert isinstance(chunk.usage, Usage)
assert chunk.usage.completion_tokens == 206
assert chunk.usage.prompt_tokens == 62322
assert chunk.usage.total_tokens == 62528
def test_validate_chunk_without_usage_keeps_none():
payload = _final_chunk_payload()
del payload["usage"]
chunk = _StreamParser._validate_chunk(payload)
assert chunk.usage is None
def test_validated_usage_survives_nested_model_dump():
"""The original crash: the raw openai-SDK usage object attached to a
ModelResponseStream made model_dump() raise
"TypeError: 'MockValSer' object is not an instance of 'SchemaSerializer'"."""
chunk = _StreamParser._validate_chunk(_final_chunk_payload())
model_response = ModelResponseStream()
setattr(model_response, "usage", chunk.usage)
dumped = model_response.model_dump() # must not raise
assert dumped["usage"]["total_tokens"] == 62528
def test_to_openai_chunk_normalizes_openai_shaped_event():
"""An already-openai-shaped event goes through the same normalization."""
chunk = _StreamParser.to_openai_chunk(_final_chunk_payload())
assert chunk is not None
assert chunk.choices[0].logprobs is None
assert isinstance(chunk.usage, Usage)
def test_to_openai_chunk_from_orchestration_result():
"""An orchestration_result delta event is mapped and normalized into a chunk."""
event = {
"request_id": "a07127d3-cb74-9427-a4dc-ef9bf424fb43",
"orchestration_result": {
"id": "chatcmpl-sap-delta",
"object": "chat.completion.chunk",
"created": 1761319270,
"model": "anthropic--claude-4.7-opus",
"choices": [
{
"index": 0,
"delta": {"role": "assistant", "content": "Hello "},
"logprobs": {},
"finish_reason": None,
}
],
},
}
chunk = _StreamParser.to_openai_chunk(event)
assert chunk is not None
assert chunk.choices[0].delta.content == "Hello "
assert chunk.choices[0].logprobs is None
def test_deployment_url_raises_404_when_no_orchestration_deployment():
from litellm.llms.sap.chat.transformation import GenAIHubOrchestrationConfig
config = GenAIHubOrchestrationConfig()
config.token_creator = lambda: "Bearer FAKE_TOKEN"
config._base_url = "https://api.ai.mock-sap.com/v2"
config._resource_group = "fake-group"
mock_client = MagicMock()
mock_client.get.return_value.json.return_value = {"resources": []}
config._http_client = mock_client
with pytest.raises(GenAIHubOrchestrationError) as exc_info:
_ = config.deployment_url
assert exc_info.value.status_code == 404
assert "fake-group" in exc_info.value.message

View file

@ -639,3 +639,18 @@ class TestSAPTransformationIntegration:
config["config"]["modules"][1]["translation"]["input"]["type"]
== "sap_document_translation"
)
def test_deployment_url_raises_404_when_no_orchestration_deployment(self, mock_config):
from unittest.mock import MagicMock
from litellm.llms.sap.chat.handler import GenAIHubOrchestrationError
mock_client = MagicMock()
mock_client.get.return_value.json.return_value = {"resources": []}
mock_config._http_client = mock_client
with pytest.raises(GenAIHubOrchestrationError) as exc_info:
_ = mock_config.deployment_url
assert exc_info.value.status_code == 404
assert "test-group" in exc_info.value.message