diff --git a/litellm/llms/sap/chat/models.py b/litellm/llms/sap/chat/models.py index 44ff85c6170..b4e7fd2b352 100644 --- a/litellm/llms/sap/chat/models.py +++ b/litellm/llms/sap/chat/models.py @@ -1,4 +1,5 @@ -from typing import Union, Literal +from typing import Union, Literal, Optional +from enum import Enum from pydantic import BaseModel, Field, field_validator, model_validator, ValidationError @@ -20,7 +21,7 @@ def validate_different_content(v: Union[str, dict, list]) -> str: elif isinstance(v, str): return v raise ValueError("Content must be a string") - return v + class TextContent(BaseModel): @@ -113,6 +114,7 @@ class SAPToolChatMessage(BaseModel): validate_different_content ) + ChatMessage = Union[SAPUserMessage, SAPAssistantMessage, SAPToolChatMessage, SAPMessage] @@ -155,11 +157,11 @@ class GroundingSearchConfig(BaseModel): 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 + search_config: Optional[GroundingSearchConfig] = None + data_repositories: Optional[list[str]]= None + data_repository_metadata: Optional[list[KeyValueListPair]] = None + document_metadata: Optional[list[DocumentMetadataKeyValueListPairs]] = None + chunk_metadata: Optional[list[KeyValueListPair]] = None class DocumentGroundingPlaceholders(BaseModel): @@ -168,9 +170,9 @@ class DocumentGroundingPlaceholders(BaseModel): class DocumentGroundingConfig(BaseModel): - filters: list[DocumentGroundingFilter] = None + filters: Optional[list[DocumentGroundingFilter]] = None placeholders: DocumentGroundingPlaceholders - metadata_params: list[str] = None + metadata_params: Optional[list[str]] = None class GroundingModuleConfig(BaseModel): @@ -180,15 +182,15 @@ class GroundingModuleConfig(BaseModel): class Template(BaseModel): template: list[ChatMessage] - defaults: dict = None - response_format: ResponseFormat | ResponseFormatJSONSchema = None - tools: list[ChatCompletionTool] = None + defaults: Optional[dict[str, str]] = None + response_format: Optional[Union[ResponseFormat, ResponseFormatJSONSchema]] = None + tools: Optional[list[ChatCompletionTool]] = None class LLMModelDetails(BaseModel): name: str version: str = "latest" - params: dict = None + params: Optional[dict] = None class PromptTemplatingModuleConfig(BaseModel): @@ -196,44 +198,486 @@ class PromptTemplatingModuleConfig(BaseModel): model: LLMModelDetails +class SAPMaskingProfileEntity(str, Enum): + """ + Enumerates the entity categories that can be masked by the SAP Data Privacy Integration service. + + This enum lists different types of personal or sensitive information (PII) that can be detected and masked + by the data masking module, such as personal details, organizational data, contact information, and identifiers. + + Values: + PERSON: Represents personal names. + + ORG: Represents organizational names. + + UNIVERSITY: Represents educational institutions. + + LOCATION: Represents geographical locations. + + EMAIL: Represents email addresses. + + PHONE: Represents phone numbers. + + ADDRESS: Represents physical addresses. + + SAP_IDS_INTERNAL: Represents internal SAP identifiers. + + SAP_IDS_PUBLIC: Represents public SAP identifiers. + + URL: Represents URLs. + + USERNAME_PASSWORD: Represents usernames and passwords. + + NATIONAL_ID: Represents national identification numbers. + + IBAN: Represents International Bank Account Numbers. + + SSN: Represents Social Security Numbers. + + CREDIT_CARD_NUMBER: Represents credit card numbers. + + PASSPORT: Represents passport numbers. + + DRIVING_LICENSE: Represents driving license numbers. + + NATIONALITY: Represents nationality information. + + RELIGIOUS_GROUP: Represents religious group affiliation. + + POLITICAL_GROUP: Represents political group affiliation. + + PRONOUNS_GENDER: Represents pronouns and gender identity. + + GENDER: Represents gender information. + + SEXUAL_ORIENTATION: Represents sexual orientation. + + TRADE_UNION: Represents trade union membership. + + SENSITIVE_DATA: Represents any other sensitive information. + """ + + PERSON = "profile-person" + ORG = "profile-org" + UNIVERSITY = "profile-university" + LOCATION = "profile-location" + EMAIL = "profile-email" + PHONE = "profile-phone" + ADDRESS = "profile-address" + SAP_IDS_INTERNAL = "profile-sapids-internal" + SAP_IDS_PUBLIC = "profile-sapids-public" + URL = "profile-url" + USERNAME_PASSWORD = "profile-username-password" + NATIONAL_ID = "profile-nationalid" + IBAN = "profile-iban" + SSN = "profile-ssn" + CREDIT_CARD_NUMBER = "profile-credit-card-number" + PASSPORT = "profile-passport" + DRIVING_LICENSE = "profile-driverlicense" + NATIONALITY = "profile-nationality" + RELIGIOUS_GROUP = "profile-religious-group" + POLITICAL_GROUP = "profile-political-group" + PRONOUNS_GENDER = "profile-pronouns-gender" + GENDER = "profile-gender" + SEXUAL_ORIENTATION = "profile-sexual-orientation" + TRADE_UNION = "profile-trade-union" + SENSITIVE_DATA = "profile-sensitive-data" + ETHNICITY = "profile-ethnicity" + + +class DPIMethodConstant(BaseModel): + """ + Replaces the entity with the specified value followed by an incrementing number + """ + method: str = "constant" + value: str + + +class DPIMethodFabricatedData(BaseModel): + """ + Replaces the entity with a randomly generated value appropriate to its type. + """ + method: str = "fabricated_data" + + +class DPICustomEntity(BaseModel): + """ + regex: Regular expression to match the entity + replacement_strategy: Replacement strategy to be used for the entity + """ + regex: str + replacement_strategy: DPIMethodConstant + + +class DPIStandardEntity(BaseModel): + """ + type: Standard entity type to be masked + replacement_strategy: Replacement strategy to be used for the entity + """ + type_: SAPMaskingProfileEntity = Field(..., alias="type") + replacement_strategy: Optional[Union[DPIMethodConstant, DPIMethodFabricatedData]] = None + + +class MaskGroundingInput(BaseModel): + """ + Controls whether the input to the grounding module will be masked with the configuration + supplied in the masking module + """ + enabled: bool = False + + +class MaskingProviderConfig(BaseModel): + """ + SAP Data Privacy Integration provider for data masking. + + This class implements the SAP Data Privacy Integration service, which can anonymize or pseudonymize + specified entity categories in the input data. It supports masking sensitive information like personal names, + contact details, and identifiers. + + Args: + method: The method of masking to apply (anonymization or pseudonymization). + + entities: A list of entity categories to be masked, such as names, locations, or emails. + + allowlist: A list of strings that should not be masked. + + mask_grounding_input: A flag indicating whether to mask input to the grounding module. + """ + type_: str = Field(default="sap_data_privacy_integration", alias="type") + method: Literal["anonymization", "pseudonymization"] + entities: list[Union[DPIStandardEntity, DPICustomEntity]] + allowlist: Optional[list[str]] = None + mask_grounding_input: Optional[MaskGroundingInput] = None + + +class MaskingModuleConfig(BaseModel): + """ + Configuration for the data masking module. + + Args: + providers: list of masking service provider configurations + masking_providers: list of masking provider configurations + IMPORTANT: use exactly one of the parameters to set the list of masking provider configurations. + DEPRECATED: parameter 'masking_providers' will be removed Sept 15, 2026. Use 'providers' instead. + """ + providers: Optional[list[MaskingProviderConfig]] = Field(min_length=1, default=None) + masking_providers: Optional[list[MaskingProviderConfig]] = Field(min_length=1, default=None) + + @model_validator(mode="after") + def enforce_exactly_one_provider_list(self): + + has_providers = self.providers is not None + has_masking_providers = self.masking_providers is not None + + if has_providers == has_masking_providers: + raise ValueError( + "For SAP Masking Module Config must set exactly one of: 'providers' or 'masking_providers' " + "DEPRECATED: parameter 'masking_providers' will be removed Sept 15, 2026. Use 'providers' instead." + ) + + return self + + +class AzureThreshold(int, Enum): + """ + Enumerates the threshold levels for the Azure Content Safety service. + + This enum defines the various threshold levels that can be used to filter + content based on its safety score. Each threshold value represents a specific + level of content moderation. + + Values: + ALLOW_SAFE: Allows only Safe content. + + ALLOW_SAFE_LOW: Allows Safe and Low content. + + ALLOW_SAFE_LOW_MEDIUM: Allows Safe, Low, and Medium content. + + ALLOW_ALL: Allows all content (Safe, Low, Medium, and High). + """ + + ALLOW_SAFE = 0 + ALLOW_SAFE_LOW = 2 + ALLOW_SAFE_LOW_MEDIUM = 4 + ALLOW_ALL = 6 + + +class AzureContentFilter(BaseModel): + """ + Specific filter configuration for Azure Content Safety. + + This class configures content filtering based on Azure's categories and + severity levels. It allows setting thresholds for hate speech, sexual content, + violence, and self-harm content. + + Values: + hate: Threshold for hate speech content. + + sexual: Threshold for sexual content. + + violence: Threshold for violent content. + + self_harm: Threshold for self-harm content. + """ + + hate: Optional[Union[AzureThreshold, Literal[0, 2, 4, 6]]] = None + sexual: Optional[Union[AzureThreshold, Literal[0, 2, 4, 6]]] = None + violence: Optional[Union[AzureThreshold, Literal[0, 2, 4, 6]]] = None + self_harm: Optional[Union[AzureThreshold, Literal[0, 2, 4, 6]]] = None + + +class AzureContentSafetyInput(AzureContentFilter): + """ + Filter configuration for Azure Content Safety Input + + Values: + hate: Threshold for hate speech content. + + sexual: Threshold for sexual content. + + violence: Threshold for violent content. + + self_harm: Threshold for self-harm content. + + prompt_shield: A flag to use prompt shield + """ + prompt_shield: Optional[bool] = False + + +class AzureContentSafetyOutput(AzureContentFilter): + """ + Filter configuration for Azure Content Safety Output + + Values: + hate: Threshold for hate speech content. + + sexual: Threshold for sexual content. + + violence: Threshold for violent content. + + self_harm: Threshold for self-harm content. + + protected_material_code: Detect protected code content from known GitHub repositories. + The scan includes software libraries, source code, algorithms, + and other proprietary programming content. + """ + + protected_material_code: Optional[bool] = False + + +class LlamaGuard38bFilter(BaseModel): + """ + Specific implementation of ContentFilter for Llama Guard 3. Llama Guard 3 is a + Llama-3.1-8B pretrained model, fine-tuned for content safety classification. + + Args: + violent_crimes: Responses that enable, encourage, or endorse the commission of violent crimes. + + non_violent_crimes: Responses that enable, encourage, or endorse the commission of non-violent crimes. + + sex_crimes: Responses that enable, encourage, or endorse the commission of sex-related crimes. + + child_exploitation: Responses that contain, describe, enable or endorse the sexual abuse of children. + + defamation: Responses that are both verifiably false and likely to injure a living person's reputation. + + specialized_advice: Responses that contain specialized financial, medical or legal advice. + + privacy: Responses that contain sensitive or nonpublic personal information. + + intellectual_property: Responses that may violate the intellectual property rights of any third party. + + indiscriminate_weapons: Responses that enable, encourage, or endorse the creation of indiscriminate weapons. + + hate: Responses that demean or dehumanize people on the basis of their sensitive, personal characteristics. + + self_harm: Responses that enable, encourage, or endorse acts of intentional self-harm. + + sexual_content: Responses that contain erotica. + + elections: Responses that contain factually incorrect information about electoral systems and processes. + + code_interpreter_abuse: Responses that seek to abuse code interpreters. + """ + + violent_crimes: bool = Field(default=False) + non_violent_crimes: bool = Field(default=False) + sex_crimes: bool = Field(default=False) + child_exploitation: bool = Field(default=False) + defamation: bool = Field(default=False) + specialized_advice: bool = Field(default=False) + privacy: bool = Field(default=False) + intellectual_property: bool = Field(default=False) + indiscriminate_weapons: bool = Field(default=False) + hate: bool = Field(default=False) + self_harm: bool = Field(default=False) + sexual_content: bool = Field(default=False) + elections: bool = Field(default=False) + code_interpreter_abuse: bool = Field(default=False) + + +class LlamaGuard38bFilterConfig(BaseModel): + type_: str = Field(default="llama_guard_3_8b", alias="type") + config: LlamaGuard38bFilter + + +class AzureContentSafetyInputFilterConfig(BaseModel): + type_: str = Field(default="azure_content_safety", alias="type") + config: Optional[AzureContentSafetyInput] = None + + +class AzureContentSafetyOutputFilterConfig(BaseModel): + type_: str = Field(default="azure_content_safety", alias="type") + config: Optional[AzureContentSafetyOutput] = None + + +class FilteringStreamOptions(BaseModel): + """ + overlap: Number of characters that should be additionally sent to content filtering services + from previous chunks as additional context. + """ + overlap: Optional[int] = Field(default=0, ge=0, le=10000) + + +class InputFiltering(BaseModel): + """Module for managing and applying input content filters. + + Args: + filters: List of ContentFilter objects to be applied to input content. + """ + filters: list[Union[AzureContentSafetyInputFilterConfig, LlamaGuard38bFilterConfig]] = Field(min_length=1) + + +class OutputFiltering(BaseModel): + """Module for managing and applying output content filters. + + Args: + filters: List of ContentFilter objects to be applied to output content. + + stream_options: Module-specific streaming options. + """ + + filters: list[ + Union[AzureContentSafetyOutputFilterConfig, LlamaGuard38bFilterConfig]] = Field(min_length=1) + stream_options: Optional[FilteringStreamOptions] = None + + +class FilteringModuleConfig(BaseModel): + """Module for managing and applying content filters. + + Args: + input: Module for filtering and validating input content before processing. + + output: Module for filtering and validating output content after generation. + """ + input: Optional[InputFiltering] = None + output: Optional[OutputFiltering] = None + + @model_validator(mode="after") + def enforce_min_properties(cls, values): # pylint: disable=no-self-argument + """ + Ensure at least one of input or output filtering is provided. + """ + assert values.input is not None or values.output is not None, \ + "For using SAP Filtering Module you must provide at least one property: input or output filters." + return values + + +class SAPDocumentTranslationApplyToSelector(BaseModel): + """ + This selector allows you to define the scope of translation, such as specific placeholders or + messages with specific roles. + For example, {"category": "placeholders", + "items": ["user_input"], + "source_language": "de-DE"} + targets the value of "user_input" in placeholder_values specified in the request payload; + and considers the value to be in German. + """ + category: Literal["placeholders", "template_roles"] + items: list[str] + source_language: str + + +class InputTranslationConfig(BaseModel): + """ + Configuration for input translation. + + Args: + source_language: Language of the text to be translated. Example: de-DE + target_language: Language to which the text should be translated. Example: en-US + apply_to: List of selectors that define the scope of translation. + """ + source_language: Optional[str] = None + target_language: str + apply_to: Optional[list[SAPDocumentTranslationApplyToSelector]] = None + + +class OutputTranslationConfig(BaseModel): + source_language: Optional[str] = None + target_language: Union[str, SAPDocumentTranslationApplyToSelector] + + +class SAPDocumentTranslationInput(BaseModel): + """ + Configuration for input translation + + Args: + type: The type of translation module (e.g., 'sap_document_translation'). + + translate_messages_history: If true, the messages history will be translated as well. + + config: Configuration object for the translation module. + """ + type_: str = Field(default="sap_document_translation", alias="type") + translate_messages_history: Optional[bool] = None + config: InputTranslationConfig + + +class SAPDocumentTranslationOutput(BaseModel): + """ + Configuration for output translation + + Args: + type: The type of translation module (e.g., 'sap_document_translation'). + + config: Configuration object for the translation module. + """ + type_: str = Field(default="sap_document_translation", alias="type") + config: OutputTranslationConfig + + +class TranslationModuleConfig(BaseModel): + """ + Configuration for translation module + + Args: + input: Configuration for input translation + + output: Configuration for output translation + """ + input: Optional[SAPDocumentTranslationInput] = None + output: Optional[SAPDocumentTranslationOutput] = None + + class ModuleConfig(BaseModel): prompt_templating: PromptTemplatingModuleConfig - # filtering: Optional[FilteringModuleConfig] = None - # masking: Optional[MaskingModuleConfig] = None - grounding: GroundingModuleConfig = None - # translation: Optional[TranslationModuleConfig] = None + filtering: Optional[FilteringModuleConfig] = None + masking: Optional[MaskingModuleConfig] = None + grounding: Optional[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 + delimiters: Optional[list[str]] = None class OrchestrationConfig(BaseModel): modules: ModuleConfig - stream: GlobalStreamOptions = None + stream: Optional[GlobalStreamOptions] = None class OrchestrationRequest(BaseModel): config: OrchestrationConfig - placeholder_values: dict = None + placeholder_values: Optional[dict[str, str]] = None diff --git a/litellm/llms/sap/chat/transformation.py b/litellm/llms/sap/chat/transformation.py index 2bbf4423e7f..35e4572f1fd 100755 --- a/litellm/llms/sap/chat/transformation.py +++ b/litellm/llms/sap/chat/transformation.py @@ -218,20 +218,7 @@ class GenAIHubOrchestrationConfig(OpenAIGPTConfig): litellm_params: dict, headers: dict, ) -> dict: - # Filter out parameters that are not valid model params for SAP Orchestration API - # - tools, model_version, deployment_url: handled separately - excluded_params = {"tools", "model_version", "deployment_url"} - - # Filter strict for GPT models only - SAP AI Core doesn't accept it as a model param - # LangChain agents pass strict=true at top level, which fails for GPT models - # Anthropic models accept strict, so preserve it for them - if model.startswith("gpt"): - excluded_params.add("strict") - - model_params = { - k: v for k, v in optional_params.items() if k not in {"tools", "model_version", "deployment_url", "grounding", "placeholder_values"} - } - + optional_params.pop("deployment_url", None) model_version = optional_params.pop("model_version", "latest") template = messages @@ -241,7 +228,7 @@ class GenAIHubOrchestrationConfig(OpenAIGPTConfig): else: tools = {} - response_format = model_params.pop("response_format", {}) + response_format = optional_params.pop("response_format", {}) resp_type = response_format.get("type", None) if resp_type: if resp_type == "json_schema": @@ -251,44 +238,51 @@ class GenAIHubOrchestrationConfig(OpenAIGPTConfig): else: response_format = validate_dict(response_format, ResponseFormat) response_format = {"response_format": response_format} - model_params.pop("stream", False) + optional_params.pop("stream", False) stream_config = {} - if "stream_options" in model_params: - # stream_config["enabled"] = True - stream_options = model_params.pop("stream_options", {}) + if "stream_options" in optional_params: + stream_options = optional_params.pop("stream_options", {}) stream_config["chunk_size"] = stream_options.get("chunk_size", 100) if "delimiters" in stream_options: stream_config["delimiters"] = stream_options.get("delimiters") - # else: - # stream_config["enabled"] = False + + placeholder_defaults = optional_params.pop("placeholder_defaults", {}) + if placeholder_defaults: + placeholder_defaults = {"defaults": placeholder_defaults} + + placeholder_values = optional_params.pop("placeholder_values", {}) + if placeholder_values: + placeholder_values = {"placeholder_values": placeholder_values} + + optional_modules = {} + optional_modules_lst = ["grounding", "masking", "filtering", "translation"] + for module in optional_modules_lst: + if optional_params.get(module, None): + optional_modules[module] = optional_params.pop(module) + request_body = { "config": { "modules": { "prompt_templating": { - "prompt": {"template": template, **tools, **response_format}, + "prompt": { + "template": template, + **placeholder_defaults, + **tools, + **response_format + }, "model": { "name": model, - "params": model_params, + "params": optional_params, "version": model_version, }, }, + **optional_modules }, "stream": stream_config, - } + }, + **placeholder_values, } - 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