mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-21 00:21:49 +00:00
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:
parent
77890478a1
commit
264d05b1fa
4 changed files with 125 additions and 146 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue