From 5211e7a121db9fcec2185642afc342ae9b6a555f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alperen=20K=C3=B6m=C3=BCrc=C3=BC?= Date: Thu, 12 Feb 2026 12:14:43 +0100 Subject: [PATCH] sap - add additional parameters for grounding - additional parameter for grounding added for the sap provider --- litellm/llms/sap/chat/models.py | 111 +++++++++++++++++++++++- litellm/llms/sap/chat/transformation.py | 34 +++++--- 2 files changed, 130 insertions(+), 15 deletions(-) diff --git a/litellm/llms/sap/chat/models.py b/litellm/llms/sap/chat/models.py index 8ca2aa7a690..55c32099554 100644 --- a/litellm/llms/sap/chat/models.py +++ b/litellm/llms/sap/chat/models.py @@ -1,6 +1,6 @@ from typing import Union, Literal -from pydantic import BaseModel, Field, field_validator +from pydantic import BaseModel, Field, field_validator, model_validator, ValidationError def validate_different_content(v: Union[str, dict, list]) -> str: @@ -113,6 +113,8 @@ class SAPToolChatMessage(BaseModel): validate_different_content ) +ChatMessage = Union[SAPUserMessage, SAPAssistantMessage, SAPToolChatMessage, SAPMessage] + class ResponseFormat(BaseModel): type_: Literal["text", "json_object"] = Field(default="text", alias="type") @@ -128,3 +130,110 @@ class JSONResponseSchema(BaseModel): class ResponseFormatJSONSchema(BaseModel): type_: Literal["json_schema"] = Field(default="json_schema", alias="type") json_schema: JSONResponseSchema + + +class KeyValueListPair(BaseModel): + key: str + value: list[str] + + +class DocumentMetadataKeyValueListPairs(KeyValueListPair): + select_mode: list[Literal['ignoreIfKeyAbsent']] = None + + +class GroundingSearchConfig(BaseModel): + max_chunk_count: int = Field(default=None, ge=0) + max_document_count: int = Field(default=None, ge=0) + + @model_validator(mode='after') + def validate_max_chunk_count_and_max_document_count(self): + if self.max_chunk_count and self.max_document_count: + raise ValidationError("Cannot specify both maxChunkCount and maxDocumentCount.") + return self + + +class DocumentGroundingFilter(BaseModel): + id_: str = Field(default=None, alias="id") + data_repository_type: Literal["vector", "help.sap.com"] + search_config: GroundingSearchConfig = None + data_repositories: list[str] = None + data_repository_metadata: list[KeyValueListPair] = None + document_metadata: list[DocumentMetadataKeyValueListPairs] = None + chunk_metadata: list[KeyValueListPair] = None + + +class DocumentGroundingPlaceholders(BaseModel): + input: list[str] = Field(min_length=1) + output: str + + +class DocumentGroundingConfig(BaseModel): + filters: list[DocumentGroundingFilter] = None + placeholders: DocumentGroundingPlaceholders + metadata_params: list[str] = None + + +class GroundingModuleConfig(BaseModel): + type_: Literal["document_grounding_service"] = Field(default="document_grounding_service", alias="type") + config: DocumentGroundingConfig + + +class Template(BaseModel): + template: list[ChatMessage] + defaults: dict = None + response_format: ResponseFormat | ResponseFormatJSONSchema = None + tools: list[FunctionTool] = None + + +class LLMModelDetails(BaseModel): + name: str + version: str = "latest" + params: dict = None + + +class PromptTemplatingModuleConfig(BaseModel): + prompt: Template + model: LLMModelDetails + + +class ModuleConfig(BaseModel): + prompt_templating: PromptTemplatingModuleConfig + # filtering: Optional[FilteringModuleConfig] = None + # masking: Optional[MaskingModuleConfig] = None + grounding: GroundingModuleConfig = None + # translation: Optional[TranslationModuleConfig] = None + + +class GlobalStreamOptions(BaseModel): + enabled: bool = False + chunk_size: int = 100 + delimiters: list[str] = None + + @model_validator(mode='after') + def validate_streaming_params(self): + """Validate that chunk_size and delimiters are not set when enabled is False.""" + if not self.enabled: + if self.chunk_size != 100: # Check if chunk_size was explicitly set + raise ValueError("chunk_size cannot be set when enabled is False") + if self.delimiters is not None: + raise ValueError("delimiters cannot be set when enabled is False") + return self + + def model_dump(self, **kwargs): + """Override model_dump to exclude chunk_size and delimiters when enabled is False.""" + data = super().model_dump(**kwargs) + if not self.enabled: + # Remove chunk_size and delimiters from output when streaming is disabled + data.pop('chunk_size', None) + data.pop('delimiters', None) + return data + + +class OrchestrationConfig(BaseModel): + modules: ModuleConfig + stream: GlobalStreamOptions = None + + +class OrchestrationRequest(BaseModel): + config: OrchestrationConfig + placeholder_values: dict = None diff --git a/litellm/llms/sap/chat/transformation.py b/litellm/llms/sap/chat/transformation.py index 7f6bab4a1d5..2bbf4423e7f 100755 --- a/litellm/llms/sap/chat/transformation.py +++ b/litellm/llms/sap/chat/transformation.py @@ -38,6 +38,8 @@ from .models import ( ResponseFormatJSONSchema, ResponseFormat, SAPUserMessage, + GroundingModuleConfig, + OrchestrationRequest ) from .handler import ( GenAIHubOrchestrationError, @@ -227,23 +229,13 @@ class GenAIHubOrchestrationConfig(OpenAIGPTConfig): excluded_params.add("strict") model_params = { - k: v for k, v in optional_params.items() if k not in excluded_params + k: v for k, v in optional_params.items() if k not in {"tools", "model_version", "deployment_url", "grounding", "placeholder_values"} } model_version = optional_params.pop("model_version", "latest") - template = [] - for message in messages: - if message["role"] == "user": - template.append(validate_dict(message, SAPUserMessage)) - elif message["role"] == "assistant": - template.append(validate_dict(message, SAPAssistantMessage)) - elif message["role"] == "tool": - template.append(validate_dict(message, SAPToolChatMessage)) - else: - template.append(validate_dict(message, SAPMessage)) + template = messages tools_ = optional_params.pop("tools", []) - tools_ = [validate_dict(tool, ChatCompletionTool) for tool in tools_] if tools_ != []: tools = {"tools": tools_} else: @@ -269,7 +261,7 @@ class GenAIHubOrchestrationConfig(OpenAIGPTConfig): stream_config["delimiters"] = stream_options.get("delimiters") # else: # stream_config["enabled"] = False - config = { + request_body = { "config": { "modules": { "prompt_templating": { @@ -285,7 +277,21 @@ class GenAIHubOrchestrationConfig(OpenAIGPTConfig): } } - return config + placeholder_defaults = optional_params.pop("placeholder_defaults", {}) + if placeholder_defaults: + request_body["config"]["modules"]["prompt_templating"]["prompt"]["defaults"] = placeholder_defaults + + placeholder_values = optional_params.pop("placeholder_values", {}) + if placeholder_values: + request_body["placeholder_values"] = placeholder_values + + grounding_config = optional_params.pop("grounding", {}) + if grounding_config: + request_body["config"]["modules"]["grounding"] = grounding_config + + validate_dict(request_body, OrchestrationRequest) + + return request_body def transform_response( self,