diff --git a/docs/my-website/docs/providers/sap.md b/docs/my-website/docs/providers/sap.md index fb7ca0e8f96..8c76aca5af9 100644 --- a/docs/my-website/docs/providers/sap.md +++ b/docs/my-website/docs/providers/sap.md @@ -220,6 +220,17 @@ model="sap/gemini-2.5-pro" # Incorrect - missing prefix model="gpt-4o" # ❌ Won't work ``` +3. **Environment variables** - Set the following list of credentials in .env file +
+AICORE_AUTH_URL = "https://* * * .authentication.sap.hana.ondemand.com/oauth/token",
+AICORE_CLIENT_ID  = " *** ",
+AICORE_CLIENT_SECRET = " *** ",
+AICORE_RESOURCE_GROUP = " *** ",
+AICORE_BASE_URL = "https://api.ai.***.cfapps.sap.hana.ondemand.com/v2"
+
+ +Other credential configuration options are also available. For more information, see the [SAP AI Core Documentation](https://help.sap.com/doc/generative-ai-hub-sdk/CLOUD/en-US/_reference/README_sphynx.html#configuration). +## Usage - LiteLLM Python SDK ### Proxy Usage diff --git a/litellm/llms/sap/credentials.py b/litellm/llms/sap/credentials.py index ca925bc6a9a..97704882d24 100644 --- a/litellm/llms/sap/credentials.py +++ b/litellm/llms/sap/credentials.py @@ -10,6 +10,7 @@ import tempfile from litellm import sap_service_key from litellm.llms.custom_httpx.http_handler import _get_httpx_client +from litellm._logging import verbose_logger AUTH_ENDPOINT_SUFFIX = "/oauth/token" @@ -154,6 +155,7 @@ def _resolve_value( env: Dict[str, str], config: Dict[str, Any], service_like: Optional[Dict[str, Any]], + vcap_service: Optional[Dict[str, Any]] ) -> Optional[str]: # 1) explicit kwargs if cred.name in kwargs and kwargs[cred.name] is not None: @@ -165,22 +167,35 @@ def _resolve_value( val = _get_nested(service_like, cred.vcap_key) if val is not None: return val - except KeyError as e: - raise KeyError(f"Unable to find {cred.name} in service key") from e + except KeyError: + verbose_logger.debug(f"Unable to find {cred.name} in service key") + return None except json.JSONDecodeError: - raise KeyError(f"{service_like} is not valid JSON. Please fix or remove it!") + raise KeyError("service key variable is not valid JSON. Please fix or remove it!") # 3) environment variables (primary name) env_key = _env_name(cred.name) if env_key in env and env[env_key] is not None: return env[env_key] - # 4) config file (accept both prefixed and plain keys) + # 4) VCAP service + if vcap_service and cred.vcap_key: + try: + val = _get_nested(vcap_service, ("credentials",) + cred.vcap_key) + if val is not None: + return val + except KeyError: + verbose_logger.debug(f"Unable to find {cred.name} in vcap service") + return None + except json.JSONDecodeError: + raise KeyError("vcap service variable is not valid JSON. Please fix or remove it!") + + # 5) config file (accept both prefixed and plain keys) for key in (env_key, cred.name): if key in config and config[key] is not None: return config[key] - # 5) default + # 6) default return cred.default @@ -190,25 +205,23 @@ def fetch_credentials( """ Resolution order per key: kwargs + > service key > env (AICORE_) + > vcap service key > config (AICORE_ or plain ) - > service-like source from JSON in $AICORE_SERVICE_KEY (same structure as a VCAP service object) - falling back to service entry in $VCAP_SERVICES with label 'aicore' > default """ config = init_conf(profile) env = os.environ # snapshot for testability - service_like = None - # Prefer AICORE_SERVICE_KEY if present; otherwise fall back to the VCAP service. - service_like = service_key or sap_service_key or _load_json_env(SERVICE_KEY_ENV_VAR) or _get_vcap_service( - VCAP_AICORE_SERVICE_NAME - ) + service_like = service_key or sap_service_key or _load_json_env(SERVICE_KEY_ENV_VAR) + vcap_service = _get_vcap_service(VCAP_AICORE_SERVICE_NAME) out: Dict[str, str] = {} for cred in CREDENTIAL_VALUES: - value = _resolve_value(cred, kwargs=kwargs, env=env, config=config, service_like=service_like) # type: ignore + value = _resolve_value(cred, kwargs=kwargs, env=env, config=config, service_like=service_like, # type: ignore + vcap_service=vcap_service) if value is None: continue if cred.transform_fn: @@ -262,7 +275,9 @@ def get_token_creator( # Sanity check if not auth_url or not client_id: raise ValueError( - "fetch_credentials did not return valid 'auth_url' or 'client_id'" + "SAP AI Core credentials not found." + "Please provide credentials by setting appropriate environment variables " + "(e.g. AICORE_CLIENT_ID, AICORE_CLIENT_SECRET, etc.)" ) modes = [ @@ -272,6 +287,7 @@ def get_token_creator( ] if sum(bool(m) for m in modes) != 1: raise ValueError( + "SAP AI Core credentials are incomplete." "Invalid credentials: provide exactly one of client_secret, " "(cert_str & key_str), or (cert_file_path & key_file_path)." ) diff --git a/tests/test_litellm/llms/sap/test_sap_fetch_creds.py b/tests/test_litellm/llms/sap/test_sap_fetch_creds.py new file mode 100644 index 00000000000..f22d490e90f --- /dev/null +++ b/tests/test_litellm/llms/sap/test_sap_fetch_creds.py @@ -0,0 +1,47 @@ +import json +from litellm.llms.sap.credentials import fetch_credentials +mock_sap_service_key_dict = { + "serviceurls": + { + "AI_API_URL":"https://testurl.hana.ondemand.com/" + }, + "clientid":"mockclientid", + "clientsecret":"mockclientsecret", + "url":"https://test.sap.hana.ondemand.com/" +} +expected_creds = {'client_id': "mockclientid", + 'client_secret': "mockclientsecret", + 'auth_url': 'https://test.sap.hana.ondemand.com/oauth/token', + 'base_url': 'https://testurl.hana.ondemand.com/v2', + 'resource_group': 'gen-ai-hub-sdk'} + +mock_sap_vcap_service_key_dict = { + 'aicore': [{ + 'label': 'aicore', + 'name': 'aicore-instance', + 'instance_guid': '53ad5b47-a49a-4fec-9f0b-cd921c00b828', + 'credentials': { + 'serviceurls': { + 'AI_API_URL': 'vcap-api-url' + }, + 'url': 'vcap-auth-url', + 'clientid': 'vcap-clientid', + 'clientsecret': 'vcap-clientsecret' + } + }] +} + +def test_sap_fetch_creds_from_env_service_key(monkeypatch): + monkeypatch.setenv("AICORE_SERVICE_KEY", json.dumps(mock_sap_service_key_dict)) + creds = fetch_credentials() + assert creds == expected_creds + +def test_sap_fetch_creds_from_api_key_service_key(monkeypatch): + creds = fetch_credentials(service_key=json.dumps(mock_sap_service_key_dict)) + assert creds == expected_creds + +def test_fetch_creds_from_env_vcap_service(monkeypatch): + monkeypatch.setenv("VCAP_SERVICES", json.dumps(mock_sap_vcap_service_key_dict)) + creds = fetch_credentials() + assert creds['client_id'] == "vcap-clientid" + assert creds['client_secret'] == "vcap-clientsecret"