diff --git a/litellm/llms/sap/chat/transformation.py b/litellm/llms/sap/chat/transformation.py index aef3ebe4016..0c397d0b7a9 100755 --- a/litellm/llms/sap/chat/transformation.py +++ b/litellm/llms/sap/chat/transformation.py @@ -281,7 +281,7 @@ class GenAIHubOrchestrationConfig(OpenAIGPTConfig): placeholder_values = optional_params.pop("placeholder_values", {}) placeholder_values = {"placeholder_values": placeholder_values} if placeholder_values else {} - fallback_modules = optional_params.pop("fallback_modules", []) + fallback_modules = optional_params.pop("fallback_sap_modules", []) modules = [ _build_prompt_module( diff --git a/tests/test_litellm/llms/sap/chat/test_sap_transformation.py b/tests/test_litellm/llms/sap/chat/test_sap_transformation.py index 66d41a8c524..99e7d71679a 100644 --- a/tests/test_litellm/llms/sap/chat/test_sap_transformation.py +++ b/tests/test_litellm/llms/sap/chat/test_sap_transformation.py @@ -1,244 +1,5 @@ -from litellm.llms.sap.chat.transformation import GenAIHubOrchestrationConfig import pytest - -def test_sap_placeholder_defaults(): - config = GenAIHubOrchestrationConfig().transform_request( - model="gpt-4o", - messages=[ - {"role": "user", "content": "Hello. Answer {{ ?user_query }}"} - ], - optional_params={'deployment_url': "shouldn't be in results", - "placeholder_defaults": {"user_query": "default value"}}, - litellm_params={}, - headers={} - ) - - assert config["config"]["modules"][0]["prompt_templating"]["prompt"]["defaults"] == {"user_query": "default value"} - assert config["config"]["modules"][0]["prompt_templating"]["model"]["params"] == {} - - -def test_sap_placeholder_values(): - placeholder_values = {"user_query": "Some text"} - config = GenAIHubOrchestrationConfig().transform_request( - model="gpt-4o", - messages=[ - {"role": "user", "content": "Hello. Answer {{ ?user_query }}"} - ], - optional_params={'deployment_url': "shouldn't be in results", - "placeholder_values": placeholder_values}, - litellm_params={}, - headers={} - ) - - assert config["placeholder_values"] == placeholder_values - assert config["config"]["modules"][0]["prompt_templating"]["model"]["params"] == {} - - -def test_sap_grounding(): - grounding_config = { - 'type': 'document_grounding_service', - 'config': { - 'filters': [ - {'id': 's3-docs', - 'data_repository_type': 'vector', - 'search_config': {'max_chunk_count': 2}, - 'data_repositories': ['123456890-test'] - } - ], - 'placeholders': {'input': ['user_query'], 'output': 'grounding_response'}, - 'metadata_params': ['source', 'webUrl', 'title', 'mimeType', 'fileSuffix'] - } - } - placeholder_values = {"user_query": "Some text"} - config = GenAIHubOrchestrationConfig().transform_request( - model="gpt-4o", - messages=[ - {"role": "user", "content": "Hello. Answer {{ ?user_query }} using context: {{ ?grounding_response }}"} - ], - optional_params={'deployment_url': "shouldn't be in results", - "grounding": grounding_config, - "placeholder_values": placeholder_values}, - litellm_params={}, - headers={} - ) - assert config["config"]["modules"][0]["grounding"] == grounding_config - assert config["placeholder_values"] == placeholder_values - assert config["config"]["modules"][0]["prompt_templating"]["model"]["params"] == {} - - -def test_sap_filtering(): - filtering_config_azure = { - 'input': - { - 'filters': - [ - {'type': 'azure_content_safety', - 'config': - {'hate': 0, - 'sexual': 0, - 'violence': 0, - 'self_harm': 0 - } - } - ] - }, - 'output': - { - 'filters': - [ - {'type': 'azure_content_safety', - 'config': {'hate': 0, - 'sexual': 0, - 'violence': 0, - 'self_harm': 0 - } - } - ] - } - } - filtering_config_llama = { - 'input': - { - 'filters': - [ - { - 'type': 'llama_guard_3_8b', - 'config': {'hate': True, - "elections": True} - } - ] - }, - 'output': - { - 'filters': - [ - { - 'type': 'llama_guard_3_8b', - 'config': {'hate': True, "elections": True} - } - ] - } - } - config = GenAIHubOrchestrationConfig().transform_request( - model="gpt-4o", - messages=[{"role": "user", "content": "Hello."}], - optional_params={'deployment_url': "shouldn't be in results", - "filtering": filtering_config_azure}, - litellm_params={}, - headers={} - ) - assert config["config"]["modules"][0]["filtering"] == filtering_config_azure - assert config["config"]["modules"][0]["prompt_templating"]["model"]["params"] == {} - - config = GenAIHubOrchestrationConfig().transform_request( - model="gpt-4o", - messages=[{"role": "user", "content": "Hello."}], - optional_params={'deployment_url': "shouldn't be in results", - "filtering": filtering_config_llama}, - litellm_params={}, - headers={} - ) - assert config["config"]["modules"][0]["filtering"] == filtering_config_llama - assert config["config"]["modules"][0]["prompt_templating"]["model"]["params"] == {} - - -def test_sap_masking(): - masking_config = { - 'providers': - [ - { - 'type': 'sap_data_privacy_integration', - 'method': 'anonymization', - 'entities': [ - {'type': 'profile-address'}, - {'type': 'profile-email'}, - {'type': 'profile-phone'}, - {'type': 'profile-person'}, - {'type': 'profile-location'} - ] - } - ] - } - - config = GenAIHubOrchestrationConfig().transform_request( - model="gpt-4o", - messages=[{"role": "user", "content": "Hello."}], - optional_params={'deployment_url': "shouldn't be in results", - "masking": masking_config}, - litellm_params={}, - headers={} - ) - assert config["config"]["modules"][0]["masking"] == masking_config - assert config["config"]["modules"][0]["prompt_templating"]["model"]["params"] == {} - - -def test_sap_translation(): - translation_config = { - 'input': - {'type': 'sap_document_translation', - 'config': - {'source_language': 'en-US', - 'target_language': 'de-DE'} - }, - 'output': - {'type': 'sap_document_translation', - 'config': - {'source_language': 'de-DE', - 'target_language': 'fr-FR'} - } - } - - config = GenAIHubOrchestrationConfig().transform_request( - model="gpt-4o", - messages=[{"role": "user", "content": "Hello."}], - optional_params={'deployment_url': "shouldn't be in results", - "translation": translation_config}, - litellm_params={}, - headers={} - ) - assert config["config"]["modules"][0]["translation"] == translation_config - assert config["config"]["modules"][0]["prompt_templating"]["model"]["params"] == {} - - -def test_sap_multiple_modules(): - translation_config = { - 'input': - {'type': 'sap_document_translation', - 'config': - {'source_language': 'en-US', - 'target_language': 'de-DE'} - }, - 'output': - {'type': 'sap_document_translation', - 'config': - {'source_language': 'de-DE', - 'target_language': 'fr-FR'} - } - } - - config = GenAIHubOrchestrationConfig().transform_request( - model="gpt-4o", - messages=[{"role": "user", "content": "Hello."}], - optional_params={'deployment_url': "shouldn't be in results", - "fallback_modules": [{"model": "sap/gpt-5", - "messages": [{"role": "user", "content": "Hello world!"}], - "translation": translation_config - }] - , - }, - litellm_params={}, - headers={} - ) - assert "translation" not in config["config"]["modules"][0] - assert config["config"]["modules"][1]["translation"] == translation_config - assert config["config"]["modules"][1]["prompt_templating"]["model"]["name"] == "gpt-5" - assert config["config"]["modules"][0]["prompt_templating"]["model"]["name"] == "gpt-4o" - assert config["config"]["modules"][0]["prompt_templating"]["model"]["params"] == {} - assert config["config"]["modules"][1]["prompt_templating"]["prompt"]["template"][0]["content"] == "Hello world!" - assert config["config"]["modules"][0]["prompt_templating"]["prompt"]["template"][0]["content"] == "Hello." - - class TestSAPTransformationIntegration: """Integration tests for SAP transformation with parameter classification.""" @@ -335,21 +96,340 @@ class TestSAPTransformationIntegration: for excluded_param in test_case["expected_excluded"]: assert excluded_param not in filtered_params, f"Case {i + 1}: {excluded_param} should be excluded from model params" - try: - result = mock_config.transform_request( - model, messages, test_case["params"], {}, {} - ) - if result and "config" in result: - model_params = result["config"]["modules"][0]["prompt_templating"]["model"]["params"] + result = mock_config.transform_request( + model, messages, test_case["params"], {}, {} + ) + if result and "config" in result: + model_params = result["config"]["modules"][0]["prompt_templating"]["model"]["params"] - for excluded_param in test_case["expected_excluded"]: - assert excluded_param not in model_params, ( - f"Case {i + 1}: {excluded_param} should not be in actual model params" - ) - except AttributeError as e: - if "deployment_url" in str(e): - pass - else: - pytest.fail(f"Unexpected AttributeError: {e}") - except Exception as e: - pytest.fail(f"Unexpected exception in transform_request: {e}") + for excluded_param in test_case["expected_excluded"]: + assert excluded_param not in model_params, ( + f"Case {i + 1}: {excluded_param} should not be in actual model params" + ) + + def test_config_transform_with_response_format_json_object(self, mock_config): + expected_dict = {'config': + {'modules':[ + {'prompt_templating': + {'prompt': + {'template': + [{'role': 'user', 'content': 'First man on the moon, answer in json'}], + 'response_format': {'type': 'json_object'}}, + 'model': {'name': 'gpt-4o', 'params': {}, 'version': 'latest'} + } + }], + 'stream': {} + } + } + config = mock_config.transform_request( + model="gpt-4o", + messages=[{'role': 'user', 'content': 'First man on the moon, answer in json'}], + optional_params={'response_format': {'type': 'json_object'}, + 'deployment_url': "shouldn't be in results"}, + litellm_params={}, + headers={} + ) + assert config == expected_dict + + def test_config_transform_with_response_format_json_schema(self, mock_config): + + expected_response_format = { + 'type': 'json_schema', + 'json_schema': { + 'description': 'Schema for person information', + 'name': 'person_info', + 'schema': { + 'type': 'object', + 'properties': { + 'name': { + 'type': 'string', + 'description': "The person's full name" + }, + 'age': { + 'type': 'integer', + 'description': "The person's age in years" + }, + 'occupation': { + 'type': 'string', + 'description': "The person's job title" + } + }, + 'required': ['name', 'age', 'occupation'], + 'additionalProperties': False + }, + 'strict': True + } + } + + config = mock_config.transform_request( + model="gpt-4o", + messages=[{'role': 'user', 'content': 'First man on the moon, answer in json'}], + optional_params={'response_format': expected_response_format, + 'deployment_url': "shouldn't be in results"}, + litellm_params={}, + headers={} + ) + assert config["config"]["modules"][0]["prompt_templating"]["prompt"]["response_format"] == expected_response_format + assert len(config["config"]["modules"][0]["prompt_templating"]["model"]["params"]) == 0 + + def test_config_transform_with_stream(self, mock_config): + expected_dict = { + 'config': { + 'modules': [{ + 'prompt_templating': { + 'prompt': { + 'template': [{'role': 'user', 'content': 'Hello, how are you?'}] + }, + 'model': { + 'name': 'anthropic--claude-4-sonnet', + 'params': {}, + 'version': 'latest' + } + } + }], + 'stream': {'chunk_size': 10} + } + } + config = mock_config.transform_request( + model="anthropic--claude-4-sonnet", + messages=[{'content': 'Hello, how are you?', 'role': 'user'}], + optional_params={'stream': True, + 'stream_options': {'chunk_size': 10}, + 'model_version': 'latest', + 'deployment_url': "shouldn't be in results"}, + litellm_params={}, + headers={} + ) + + assert config == expected_dict + + def test_sap_placeholder_defaults(self, mock_config): + config = mock_config.transform_request( + model="gpt-4o", + messages=[ + {"role": "user", "content": "Hello. Answer {{ ?user_query }}"} + ], + optional_params={'deployment_url': "shouldn't be in results", + "placeholder_defaults": {"user_query": "default value"}}, + litellm_params={}, + headers={} + ) + + assert config["config"]["modules"][0]["prompt_templating"]["prompt"]["defaults"] == { + "user_query": "default value"} + assert config["config"]["modules"][0]["prompt_templating"]["model"]["params"] == {} + + def test_sap_placeholder_values(self, mock_config): + placeholder_values = {"user_query": "Some text"} + config = mock_config.transform_request( + model="gpt-4o", + messages=[ + {"role": "user", "content": "Hello. Answer {{ ?user_query }}"} + ], + optional_params={'deployment_url': "shouldn't be in results", + "placeholder_values": placeholder_values}, + litellm_params={}, + headers={} + ) + + assert config["placeholder_values"] == placeholder_values + assert config["config"]["modules"][0]["prompt_templating"]["model"]["params"] == {} + + def test_sap_grounding(self, mock_config): + grounding_config = { + 'type': 'document_grounding_service', + 'config': { + 'filters': [ + {'id': 's3-docs', + 'data_repository_type': 'vector', + 'search_config': {'max_chunk_count': 2}, + 'data_repositories': ['123456890-test'] + } + ], + 'placeholders': {'input': ['user_query'], 'output': 'grounding_response'}, + 'metadata_params': ['source', 'webUrl', 'title', 'mimeType', 'fileSuffix'] + } + } + placeholder_values = {"user_query": "Some text"} + config = mock_config.transform_request( + model="gpt-4o", + messages=[ + {"role": "user", "content": "Hello. Answer {{ ?user_query }} using context: {{ ?grounding_response }}"} + ], + optional_params={'deployment_url': "shouldn't be in results", + "grounding": grounding_config, + "placeholder_values": placeholder_values}, + litellm_params={}, + headers={} + ) + assert config["config"]["modules"][0]["grounding"] == grounding_config + assert config["placeholder_values"] == placeholder_values + assert config["config"]["modules"][0]["prompt_templating"]["model"]["params"] == {} + + def test_sap_filtering(self, mock_config): + filtering_config_azure = { + 'input': + { + 'filters': + [ + {'type': 'azure_content_safety', + 'config': + {'hate': 0, + 'sexual': 0, + 'violence': 0, + 'self_harm': 0 + } + } + ] + }, + 'output': + { + 'filters': + [ + {'type': 'azure_content_safety', + 'config': {'hate': 0, + 'sexual': 0, + 'violence': 0, + 'self_harm': 0 + } + } + ] + } + } + filtering_config_llama = { + 'input': + { + 'filters': + [ + { + 'type': 'llama_guard_3_8b', + 'config': {'hate': True, + "elections": True} + } + ] + }, + 'output': + { + 'filters': + [ + { + 'type': 'llama_guard_3_8b', + 'config': {'hate': True, "elections": True} + } + ] + } + } + config = mock_config.transform_request( + model="gpt-4o", + messages=[{"role": "user", "content": "Hello."}], + optional_params={'deployment_url': "shouldn't be in results", + "filtering": filtering_config_azure}, + litellm_params={}, + headers={} + ) + assert config["config"]["modules"][0]["filtering"] == filtering_config_azure + assert config["config"]["modules"][0]["prompt_templating"]["model"]["params"] == {} + + config = mock_config.transform_request( + model="gpt-4o", + messages=[{"role": "user", "content": "Hello."}], + optional_params={'deployment_url': "shouldn't be in results", + "filtering": filtering_config_llama}, + litellm_params={}, + headers={} + ) + assert config["config"]["modules"][0]["filtering"] == filtering_config_llama + assert config["config"]["modules"][0]["prompt_templating"]["model"]["params"] == {} + + def test_sap_masking(self, mock_config): + masking_config = { + 'providers': + [ + { + 'type': 'sap_data_privacy_integration', + 'method': 'anonymization', + 'entities': [ + {'type': 'profile-address'}, + {'type': 'profile-email'}, + {'type': 'profile-phone'}, + {'type': 'profile-person'}, + {'type': 'profile-location'} + ] + } + ] + } + + config = mock_config.transform_request( + model="gpt-4o", + messages=[{"role": "user", "content": "Hello."}], + optional_params={'deployment_url': "shouldn't be in results", + "masking": masking_config}, + litellm_params={}, + headers={} + ) + assert config["config"]["modules"][0]["masking"] == masking_config + assert config["config"]["modules"][0]["prompt_templating"]["model"]["params"] == {} + + def test_sap_translation(self, mock_config): + translation_config = { + 'input': + {'type': 'sap_document_translation', + 'config': + {'source_language': 'en-US', + 'target_language': 'de-DE'} + }, + 'output': + {'type': 'sap_document_translation', + 'config': + {'source_language': 'de-DE', + 'target_language': 'fr-FR'} + } + } + + config = mock_config.transform_request( + model="gpt-4o", + messages=[{"role": "user", "content": "Hello."}], + optional_params={'deployment_url': "shouldn't be in results", + "translation": translation_config}, + litellm_params={}, + headers={} + ) + assert config["config"]["modules"][0]["translation"] == translation_config + assert config["config"]["modules"][0]["prompt_templating"]["model"]["params"] == {} + + def test_sap_multiple_modules(self, mock_config): + translation_config = { + 'input': + {'type': 'sap_document_translation', + 'config': + {'source_language': 'en-US', + 'target_language': 'de-DE'} + }, + 'output': + {'type': 'sap_document_translation', + 'config': + {'source_language': 'de-DE', + 'target_language': 'fr-FR'} + } + } + + config = mock_config.transform_request( + model="gpt-4o", + messages=[{"role": "user", "content": "Hello."}], + optional_params={'deployment_url': "shouldn't be in results", + "fallback_sap_modules": [{"model": "sap/gpt-5", + "messages": [{"role": "user", "content": "Hello world!"}], + "translation": translation_config + }] + , + }, + litellm_params={}, + headers={} + ) + assert "translation" not in config["config"]["modules"][0] + assert config["config"]["modules"][1]["translation"] == translation_config + assert config["config"]["modules"][1]["prompt_templating"]["model"]["name"] == "gpt-5" + assert config["config"]["modules"][0]["prompt_templating"]["model"]["name"] == "gpt-4o" + assert config["config"]["modules"][0]["prompt_templating"]["model"]["params"] == {} + assert config["config"]["modules"][1]["prompt_templating"]["prompt"]["template"][0]["content"] == "Hello world!" + assert config["config"]["modules"][0]["prompt_templating"]["prompt"]["template"][0]["content"] == "Hello." \ No newline at end of file diff --git a/tests/test_litellm/llms/sap/embed/test_sap_embed_transformation.py b/tests/test_litellm/llms/sap/embed/test_sap_embed_transformation.py new file mode 100644 index 00000000000..dfb56d8c3c7 --- /dev/null +++ b/tests/test_litellm/llms/sap/embed/test_sap_embed_transformation.py @@ -0,0 +1,48 @@ +from unittest.mock import patch, PropertyMock + +import pytest + +from litellm.llms.sap.embed.transformation import GenAIHubEmbeddingConfig + +@pytest.fixture +def fake_token_creator(): + return lambda: "Bearer FAKE_TOKEN", "https://api.ai.moke-sap.com", "fake-group" + + +@pytest.fixture +def fake_deployment_url(): + return "https://api.ai.moke-sap.com/v2/inference/deployments/mokeid" + +def test_basic_config_transform(fake_token_creator, fake_deployment_url): + expected_dict = { + 'config': { + 'modules': { + 'embeddings': { + 'model': { + 'name': 'text-embedding-3-small', + 'version': 'latest', + 'params': {} + } + } + } + }, + 'input': { + 'text': 'Hi', + 'type': 'text' + } + } + with patch( + "litellm.llms.sap.embed.transformation.GenAIHubEmbeddingConfig.deployment_url", + new_callable=PropertyMock, + return_value=fake_deployment_url, + ), patch( + "litellm.llms.sap.embed.transformation.get_token_creator", + return_value=fake_token_creator, + ): + body = GenAIHubEmbeddingConfig().transform_embedding_request( + model="text-embedding-3-small", + input="Hi", + optional_params={}, + headers={} + ) + assert body == expected_dict \ No newline at end of file