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"