From 2925c07cea9b83bfbf7d54fd872b1dd4780e759c Mon Sep 17 00:00:00 2001 From: Mrun25 <165297479+Mrun25@users.noreply.github.com> Date: Sat, 17 Oct 2026 14:12:43 +0530 Subject: [PATCH] fix(sap): cache the orchestration deployment URL process-wide Cherry-picked from the PR: https://github.com/BerriAI/litellm/pulls/47102 --- litellm/llms/sap/chat/transformation.py | 27 ++++++++++++++++++------ litellm/llms/sap/embed/transformation.py | 24 ++++++++++++++++++--- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/litellm/llms/sap/chat/transformation.py b/litellm/llms/sap/chat/transformation.py index aaf1cb96566..f0d3c17370c 100755 --- a/litellm/llms/sap/chat/transformation.py +++ b/litellm/llms/sap/chat/transformation.py @@ -2,10 +2,11 @@ Translate from OpenAI's `/v1/chat/completions` to SAP Generative AI Hub's Orchestration Service`v2/completion` """ +import threading import types import os from collections.abc import AsyncIterator, Iterator, Mapping -from typing import TYPE_CHECKING, Any, Final, Union +from typing import TYPE_CHECKING, Any, ClassVar, Final, Union import httpx from pydantic import BaseModel @@ -187,6 +188,13 @@ class GenAIHubOrchestrationConfig(OpenAIGPTConfig): self.run_env_setup() return self._resource_group + # Process-wide cache: the provider map builds a fresh config instance per + # call, so an instance-scoped cached_property re-runs the two discovery + # GETs on every LLM call. Keyed on (base_url, resource_group) - the inputs + # that determine the URL - so a credential change never serves a stale URL. + _deployment_url_cache: ClassVar[dict[tuple[str, str], str]] = {} # mutable-ok: process cache, guarded by the lock + _deployment_url_lock: ClassVar[threading.Lock] = threading.Lock() + @property def deployment_url(self) -> str: """Resolve the orchestration deployment URL, with caching. @@ -200,12 +208,17 @@ class GenAIHubOrchestrationConfig(OpenAIGPTConfig): handled one level up in ``get_complete_url`` before this property is ever called. """ - cached = self._cached_deployment_url - if cached is None: - env = os.environ.get(_AICORE_ORCHESTRATION_DEPLOYMENT_URL_ENV_VAR) - cached = env or self._fetch_deployment_url() - self._cached_deployment_url = cached - return cached + cache_key: Final = (self.base_url, self.resource_group) + cached_url: Final = GenAIHubOrchestrationConfig._deployment_url_cache.get(cache_key) + if cached_url is not None: + return cached_url + with GenAIHubOrchestrationConfig._deployment_url_lock: + rechecked_url: Final = GenAIHubOrchestrationConfig._deployment_url_cache.get(cache_key) + if rechecked_url is not None: + return rechecked_url + fetched_url: Final = os.environ.get(_AICORE_ORCHESTRATION_DEPLOYMENT_URL_ENV_VAR) or self._fetch_deployment_url() + GenAIHubOrchestrationConfig._deployment_url_cache[cache_key] = fetched_url + return fetched_url def _fetch_deployment_url(self) -> str: client: Final = litellm.module_level_client diff --git a/litellm/llms/sap/embed/transformation.py b/litellm/llms/sap/embed/transformation.py index a59b7e1e00d..492a8b6c18c 100644 --- a/litellm/llms/sap/embed/transformation.py +++ b/litellm/llms/sap/embed/transformation.py @@ -2,8 +2,8 @@ Translates from OpenAI's `/v1/embeddings` to IBM's `/text/embeddings` route. """ -from functools import cached_property -from typing import Final, Literal +import threading +from typing import ClassVar, Final, Literal import httpx from pydantic import Field @@ -98,8 +98,26 @@ class GenAIHubEmbeddingConfig(BaseEmbeddingConfig): } return headers - @cached_property + # Process-wide cache, same reasoning as the chat config: per-call config + # instances would otherwise re-run the two discovery GETs on every call. + _deployment_url_cache: ClassVar[dict[tuple[str, str], str]] = {} # mutable-ok: process cache, guarded by the lock + _deployment_url_lock: ClassVar[threading.Lock] = threading.Lock() + + @property def deployment_url(self) -> str: + cache_key: Final = (self.base_url, self.resource_group) + cached_url: Final = GenAIHubEmbeddingConfig._deployment_url_cache.get(cache_key) + if cached_url is not None: + return cached_url + with GenAIHubEmbeddingConfig._deployment_url_lock: + rechecked_url: Final = GenAIHubEmbeddingConfig._deployment_url_cache.get(cache_key) + if rechecked_url is not None: + return rechecked_url + fetched_url: Final = self._fetch_deployment_url() + GenAIHubEmbeddingConfig._deployment_url_cache[cache_key] = fetched_url + return fetched_url + + def _fetch_deployment_url(self) -> str: with httpx.Client(timeout=30) as client: valid_deployments: Final = [] deployments: Final[AICoreDeploymentList] = client.get(