fix(sap): normalize stream chunks to avoid MockValSer errors and clarify missing deployment error

This commit is contained in:
ZOU Yi (BD/SWD-WDE1) 2026-07-28 16:13:43 +08:00
parent 252c71c0b2
commit 9538e216b2
2 changed files with 32 additions and 3 deletions

View file

@ -9,6 +9,7 @@ import httpx
from litellm.llms.base_llm.chat.transformation import BaseConfig, BaseLLMException
from litellm.types.llms.openai import OpenAIChatCompletionChunk
from litellm.types.utils import Usage
from ...custom_httpx.llm_http_handler import BaseLLMHTTPHandler
@ -46,6 +47,25 @@ def _is_terminal_chunk(chunk: OpenAIChatCompletionChunk) -> bool:
class _StreamParser:
"""Normalize orchestration streaming events into OpenAI-like chunks."""
@staticmethod
def _validate_chunk(payload: dict) -> 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 []:
if isinstance(choice, dict) and not choice.get("logprobs"):
choice.pop("logprobs", None)
chunk = OpenAIChatCompletionChunk.model_validate(payload)
if chunk.usage is not None:
chunk.usage = Usage(**chunk.usage.model_dump())
return chunk
@staticmethod
def _from_orchestration_result(evt: dict) -> OpenAIChatCompletionChunk | None:
"""
@ -55,7 +75,7 @@ class _StreamParser:
if not orc:
return None
return OpenAIChatCompletionChunk.model_validate(
return _StreamParser._validate_chunk(
{
"id": orc.get("id") or evt.get("request_id") or "stream-chunk",
"object": orc.get("object") or "chat.completion.chunk",
@ -93,7 +113,7 @@ class _StreamParser:
# ensure it looks like an OpenAI chunk
if "object" not in fr:
fr["object"] = "chat.completion.chunk"
return OpenAIChatCompletionChunk.model_validate(fr)
return _StreamParser._validate_chunk(fr)
# Orchestration incremental delta
if "orchestration_result" in event_obj:
@ -101,7 +121,7 @@ class _StreamParser:
# Already an OpenAI-like chunk
if "choices" in event_obj and "object" in event_obj:
return OpenAIChatCompletionChunk.model_validate(event_obj)
return _StreamParser._validate_chunk(event_obj)
# Unknown / heartbeat / metrics
return None

View file

@ -183,6 +183,15 @@ class GenAIHubOrchestrationConfig(OpenAIGPTConfig):
if cfg.get("executableId") == "orchestration":
valid.append((dep["deploymentUrl"], dep["createdAt"]))
# newest first
if not valid:
raise GenAIHubOrchestrationError(
status_code=404,
message=(
"No orchestration deployment found in SAP AI Core resource group "
f"'{self.resource_group}'. Create/start an orchestration deployment "
"in SAP AI Launchpad, then retry."
),
)
return sorted(valid, key=lambda x: x[1], reverse=True)[0][0]
@classmethod