1414from sap_cloud_sdk .core .telemetry .metrics_decorator import record_metrics
1515from sap_cloud_sdk .core .telemetry .module import Module
1616from sap_cloud_sdk .core .telemetry .operation import Operation
17- from .completion import acompletion , completion
17+ from .completion import acompletion , completion , _set_proxy_active
1818from .filtering import (
1919 AzureContentFilter ,
2020 ContentFilter ,
3131
3232logger = logging .getLogger (__name__ )
3333
34+ # When set, the infrastructure sidecar adds the mTLS certificate transparently.
35+ # The SDK calls the XSUAA token endpoint over plain HTTPS with only client_id.
36+ # No client_secret or certificate material is required in the service binding.
37+ TRANSPARENT_TLS_ENV_VAR = "AICORE_TRANSPARENT_TLS"
38+
39+ # Option 3 — transparent proxy routing.
40+ # Deployer injects these; agent code is identical in all environments.
41+ _PROXY_URL_ENV = "AICORE_PROXY_URL"
42+ _PROXY_VIRTUAL_KEY_ENV = "AICORE_PROXY_VIRTUAL_KEY"
43+ _DESTINATION_NAME_ENV = "AICORE_DESTINATION_NAME"
44+
45+
46+ def _is_transparent_tls () -> bool :
47+ """Return True when transparent TLS proxy mode is active."""
48+ return os .environ .get (TRANSPARENT_TLS_ENV_VAR , "" ).strip ().lower () in ("1" , "true" , "yes" )
49+
3450
3551def _get_secret (
3652 env_var_name : str ,
@@ -119,14 +135,25 @@ def _get_aicore_base_url(instance_name: str = "aicore-instance") -> str:
119135def set_aicore_config (instance_name : str = "aicore-instance" ) -> None :
120136 """Load AI Core credentials and activate content filtering.
121137
122- Loads secrets from files or environment variables and sets them as
123- process env vars so ``litellm`` picks them up.
138+ Detects which routing mode is active based on environment variables:
139+
140+ - ``AICORE_PROXY_URL`` set → **proxy mode**: routes all LiteLLM calls
141+ through a LiteLLM proxy; ``sap/<model>`` is aliased to
142+ ``litellm_proxy/<model>`` transparently. No AI Core credentials
143+ are written to the process environment.
144+
145+ - ``AICORE_DESTINATION_NAME`` set → **destination mode**: loads AI Core
146+ credentials from a BTP Destination Service destination at startup.
147+ The deployer only needs to inject Destination Service binding credentials;
148+ the AI Core ``client_secret`` never needs to be in the K8s Secret.
124149
125- File mappings based on the Kubernetes secret structure:
126- clientid → AICORE_CLIENT_ID
127- clientsecret → AICORE_CLIENT_SECRET
128- url → AICORE_AUTH_URL
129- serviceurls (JSON with AI_API_URL) → AICORE_BASE_URL
150+ - Neither set → **direct mode** (existing behaviour): credentials are
151+ loaded from a mounted K8s secret volume or environment variables.
152+ ``AICORE_TRANSPARENT_TLS=true`` suppresses ``client_secret`` and
153+ relies on an mTLS sidecar.
154+
155+ Agent code is identical in all three modes — the deployer controls
156+ routing by choosing which env vars to inject.
130157
131158 After credentials are loaded, content filtering is activated on every
132159 ``sap/*`` LiteLLM call at the configured thresholds (default: severity
@@ -136,44 +163,131 @@ def set_aicore_config(instance_name: str = "aicore-instance") -> None:
136163 to turn filtering off at runtime, or set ``AICORE_FILTER_ENABLED=false``
137164 to keep it off entirely.
138165 """
139- # Load secrets
166+ proxy_url = os .environ .get (_PROXY_URL_ENV , "" )
167+ destination_name = os .environ .get (_DESTINATION_NAME_ENV , "" )
168+
169+ if proxy_url :
170+ _configure_proxy_mode (proxy_url )
171+ elif destination_name :
172+ _configure_destination_mode (destination_name )
173+ else :
174+ _configure_direct_mode (instance_name )
175+
176+ set_filtering ()
177+
178+
179+ def _configure_proxy_mode (proxy_url : str ) -> None :
180+ """Configure LiteLLM to route calls through an external proxy.
181+
182+ Sets ``litellm.api_base`` / ``litellm.api_key`` globally and activates
183+ the ``sap/`` → ``litellm_proxy/`` model alias rewrite in the
184+ completion wrappers. No AI Core credentials are written to env.
185+ """
186+ import litellm as _litellm
187+
188+ virtual_key = os .environ .get (_PROXY_VIRTUAL_KEY_ENV , "" )
189+ _litellm .api_base = proxy_url
190+ if virtual_key :
191+ _litellm .api_key = virtual_key
192+ _set_proxy_active (True )
193+ logger .info ("AI Core proxy mode active — routing via %s" , proxy_url )
194+
195+
196+ def _configure_destination_mode (name : str ) -> None :
197+ """Load AI Core credentials from a BTP Destination Service destination.
198+
199+ Calls the Destination Service at startup to resolve the named destination
200+ and extracts ``clientId``, ``clientSecret``, ``tokenServiceURL``, and the
201+ AI Core ``URL`` from the destination configuration properties. These are
202+ written to the standard ``AICORE_*`` env vars so that LiteLLM can fetch
203+ an OAuth token from XSUAA as usual.
204+
205+ Security: The deployer does NOT need to inject ``AICORE_CLIENT_SECRET``
206+ directly — only Destination Service binding credentials are required in
207+ the agent environment.
208+
209+ Raises ``RuntimeError`` if the destination is not found or does not
210+ return ``clientId`` / ``clientSecret``.
211+ """
212+ from sap_cloud_sdk .destination import create_client # lazy import
213+
214+ client = create_client ()
215+ dest = client .get_destination (name )
216+
217+ if dest is None :
218+ raise RuntimeError (
219+ f"AI Core destination '{ name } ' not found in Destination Service. "
220+ "Check that the destination exists and the binding has access."
221+ )
222+
223+ base_url = dest .url or ""
224+ if base_url and not base_url .endswith ("/v2" ):
225+ base_url = base_url .rstrip ("/" ) + "/v2"
226+ if base_url :
227+ os .environ ["AICORE_BASE_URL" ] = base_url
228+
229+ resource_group = dest .properties .get ("resource_group" , "default" )
230+ os .environ ["AICORE_RESOURCE_GROUP" ] = resource_group
231+
232+ client_id = dest .properties .get ("clientId" , "" )
233+ client_secret = dest .properties .get ("clientSecret" , "" )
234+ token_service_url = dest .properties .get ("tokenServiceURL" , "" )
235+
236+ if not client_id or not client_secret :
237+ raise RuntimeError (
238+ f"Destination '{ name } ' did not return clientId/clientSecret. "
239+ "Ensure the destination uses OAuth2ClientCredentials authentication "
240+ "and the calling app has the Destination Service technical-user scope."
241+ )
242+
243+ os .environ ["AICORE_CLIENT_ID" ] = client_id
244+ os .environ ["AICORE_CLIENT_SECRET" ] = client_secret
245+
246+ if token_service_url :
247+ if not token_service_url .endswith ("/oauth/token" ):
248+ token_service_url = token_service_url .rstrip ("/" ) + "/oauth/token"
249+ os .environ ["AICORE_AUTH_URL" ] = token_service_url
250+
251+ logger .info ("AI Core destination mode active — credentials loaded from '%s'" , name )
252+
253+
254+ def _configure_direct_mode (instance_name : str ) -> None :
255+ """Load AI Core credentials directly from mounted secrets or env vars."""
256+ transparent_tls = _is_transparent_tls ()
257+
140258 client_id = _get_secret ("AICORE_CLIENT_ID" , "clientid" , instance_name = instance_name )
141- client_secret = _get_secret (
142- "AICORE_CLIENT_SECRET" , "clientsecret" , instance_name = instance_name
143- )
144259 auth_url = _get_secret ("AICORE_AUTH_URL" , "url" , instance_name = instance_name )
145260 base_url = _get_aicore_base_url (instance_name )
146261 resource_group = _get_secret (
147262 "AICORE_RESOURCE_GROUP" , default = "default" , instance_name = instance_name
148263 )
149264
150- # Ensure AICORE_AUTH_URL has /oauth/token suffix
151265 if auth_url and not auth_url .endswith ("/oauth/token" ):
152266 auth_url = auth_url .rstrip ("/" ) + "/oauth/token"
153267
154268 if base_url and not base_url .endswith ("/v2" ):
155269 base_url = base_url .rstrip ("/" ) + "/v2"
156270
157- # Set environment variables for LiteLLM
158271 if client_id :
159272 os .environ ["AICORE_CLIENT_ID" ] = client_id
160- if client_secret :
161- os .environ ["AICORE_CLIENT_SECRET" ] = client_secret
162273 if auth_url :
163274 os .environ ["AICORE_AUTH_URL" ] = auth_url
164275 if base_url :
165276 os .environ ["AICORE_BASE_URL" ] = base_url
166277 if resource_group :
167278 os .environ ["AICORE_RESOURCE_GROUP" ] = resource_group
168279
169- # Log configuration completion (excluding sensitive information)
170- logger .info ("AI Core configuration has been set successfully" )
280+ if transparent_tls :
281+ os .environ .pop ("AICORE_CLIENT_SECRET" , None )
282+ logger .info ("AI Core transparent TLS mode active — client_secret not required" )
283+ else :
284+ client_secret = _get_secret (
285+ "AICORE_CLIENT_SECRET" , "clientsecret" , instance_name = instance_name
286+ )
287+ if client_secret :
288+ os .environ ["AICORE_CLIENT_SECRET" ] = client_secret
171289
172- # Activate content filtering for all sap/* LiteLLM model calls.
173- # AICORE_FILTER_ENABLED=false disables; AICORE_FILTER_* tune thresholds.
174- # Errors propagate — filtering misconfiguration should surface at startup
175- # rather than be swallowed silently.
176- set_filtering ()
290+ logger .info ("AI Core configuration has been set successfully" )
177291
178292
179293def _get_secret_dir_mtime (instance_name : str = "aicore-instance" ) -> float :
0 commit comments