diff --git a/litellm/llms/sap/chat/handler.py b/litellm/llms/sap/chat/handler.py index b4f4d4faddb..77fb59431e9 100755 --- a/litellm/llms/sap/chat/handler.py +++ b/litellm/llms/sap/chat/handler.py @@ -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 diff --git a/litellm/llms/sap/chat/transformation.py b/litellm/llms/sap/chat/transformation.py index d64d7a57281..affb6193287 100755 --- a/litellm/llms/sap/chat/transformation.py +++ b/litellm/llms/sap/chat/transformation.py @@ -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