77import com .google .common .collect .Iterables ;
88import com .sap .cloud .sdk .cloudplatform .connectivity .ApacheHttpClient5Accessor ;
99import com .sap .cloud .sdk .cloudplatform .connectivity .DefaultHttpDestination ;
10- import com .sap .cloud .sdk .cloudplatform .connectivity .Destination ;
11- import com .sap .cloud .sdk .cloudplatform .connectivity .DestinationProperty ;
1210import com .sap .cloud .sdk .cloudplatform .connectivity .HttpDestination ;
1311import com .sap .cloud .sdk .cloudplatform .connectivity .exception .DestinationAccessException ;
1412import com .sap .cloud .sdk .cloudplatform .connectivity .exception .DestinationNotFoundException ;
1513import com .sap .cloud .sdk .services .openapi .apiclient .ApiClient ;
1614import java .util .NoSuchElementException ;
17- import java .util .function .BiFunction ;
18- import java .util .function .Function ;
15+ import java .util .function .Supplier ;
1916import javax .annotation .Nonnull ;
17+ import lombok .AccessLevel ;
18+ import lombok .Getter ;
19+ import lombok .RequiredArgsConstructor ;
2020import lombok .extern .slf4j .Slf4j ;
2121import lombok .val ;
2222import org .springframework .http .client .BufferingClientHttpRequestFactory ;
2525import org .springframework .http .converter .json .MappingJackson2HttpMessageConverter ;
2626import org .springframework .web .client .RestTemplate ;
2727
28- /** Connectivity convenience methods for AI Core. */
28+ /**
29+ * Connectivity convenience methods for AI Core, offering convenient access to destinations
30+ * targeting the AI Core service. Loads base destinations from the environment or allows for setting
31+ * a custom base destination.
32+ */
2933@ Slf4j
30- public class AiCoreService implements AiCoreDestination {
31- static final String AI_CLIENT_TYPE_KEY = "URL.headers.AI-Client-Type" ;
32- static final String AI_CLIENT_TYPE_VALUE = "AI SDK Java" ;
33- static final String AI_RESOURCE_GROUP = "URL.headers.AI-Resource-Group" ;
34+ @ Getter (AccessLevel .PACKAGE )
35+ @ RequiredArgsConstructor (access = AccessLevel .PACKAGE )
36+ public class AiCoreService {
37+ /** The default resource group. */
38+ public static final String DEFAULT_RESOURCE_GROUP = "default" ;
3439
35- private static final DeploymentCache DEPLOYMENT_CACHE = new DeploymentCache () ;
40+ private static final String RESOURCE_GROUP_HEADER_PROPERTY = "URL.headers.AI-Resource-Group" ;
3641
37- @ Nonnull private final DestinationResolver destinationResolver ;
38-
39- @ Nonnull private Function <AiCoreService , HttpDestination > baseDestinationHandler ;
40- @ Nonnull private final BiFunction <AiCoreService , HttpDestination , ApiClient > clientHandler ;
41-
42- @ Nonnull
43- private final BiFunction <AiCoreService , HttpDestination , DefaultHttpDestination .Builder >
44- builderHandler ;
45-
46- /** The resource group is defined by AiCoreDeployment.withResourceGroup(). */
47- @ Nonnull String resourceGroup ;
48-
49- /** The deployment id is set by AiCoreDeployment.destination() or AiCoreDeployment.client(). */
50- @ Nonnull String deploymentId ;
42+ @ Nonnull private final Supplier <HttpDestination > baseDestinationResolver ;
43+ @ Nonnull private final DeploymentResolver deploymentResolver ;
5144
5245 /** The default constructor. */
5346 public AiCoreService () {
54- this (new DestinationResolver ());
55- }
56-
57- AiCoreService (@ Nonnull final DestinationResolver destinationResolver ) {
58- this .destinationResolver = destinationResolver ;
59- baseDestinationHandler = AiCoreService ::getBaseDestination ;
60- clientHandler = AiCoreService ::buildApiClient ;
61- builderHandler = AiCoreService ::getDestinationBuilder ;
62- resourceGroup = "default" ;
63- deploymentId = "" ;
64- }
65-
66- @ Nonnull
67- @ Override
68- public ApiClient client () {
69- val destination = destination ();
70- return clientHandler .apply (this , destination );
47+ val resolver = new DestinationResolver ();
48+ this .baseDestinationResolver = resolver ::getDestination ;
49+ this .deploymentResolver = new DeploymentResolver (this );
7150 }
7251
73- @ Nonnull
74- @ Override
75- public HttpDestination destination () {
76- val dest = baseDestinationHandler .apply (this );
77- val builder = builderHandler .apply (this , dest );
78- if (!deploymentId .isEmpty ()) {
79- destinationSetUrl (builder , dest );
80- destinationSetHeaders (builder );
81- }
82- return builder .build ();
52+ AiCoreService (@ Nonnull final Supplier <HttpDestination > baseDestinationResolver ) {
53+ this .baseDestinationResolver = baseDestinationResolver ;
54+ this .deploymentResolver = new DeploymentResolver (this );
8355 }
8456
8557 /**
86- * Update and set the URL for the destination.
58+ * Set a specific base destination. This is useful when loading a destination from the BTP
59+ * destination service or some other source.
8760 *
88- * @param builder The destination builder.
89- * @param dest The original destination reference.
90- */
91- protected void destinationSetUrl (
92- @ Nonnull final DefaultHttpDestination .Builder builder , @ Nonnull final Destination dest ) {
93- String uri = dest .get (DestinationProperty .URI ).get ();
94- if (!uri .endsWith ("/" )) {
95- uri = uri + "/" ;
96- }
97- builder .uri (uri + "v2/inference/deployments/%s/" .formatted (deploymentId ));
98- }
99-
100- /**
101- * Update and set the default request headers for the destination.
61+ * <p><b>Note:</b> For typical scenarios, the destination is expected to have the {@code /v2/}
62+ * base path set. But for special cases a different base path may be required (e.g. when consuming
63+ * AI Core via some proxy that expects a different base path).
10264 *
103- * @param builder The destination builder.
65+ * @param destination The base destination to be used for AI Core service calls.
66+ * @return A new AI Core Service object using the provided destination as basis.
10467 */
105- protected void destinationSetHeaders (@ Nonnull final DefaultHttpDestination .Builder builder ) {
106- builder .property (AI_RESOURCE_GROUP , resourceGroup );
68+ @ Nonnull
69+ public AiCoreService withBaseDestination (@ Nonnull final HttpDestination destination ) {
70+ return new AiCoreService (() -> DestinationResolver .fromCustomBaseDestination (destination ));
10771 }
10872
10973 /**
110- * Set a specific base destination.
74+ * Get the base destination for AI Core service calls. This destination won't have any resource group set .
11175 *
112- * @param destination The destination to be used for AI Core service calls.
113- * @return The AI Core Service based on the provided destination.
76+ * @return The base destination.
77+ * @throws DestinationAccessException If there was an issue creating the base destination, e.g. in
78+ * case of invalid credentials.
79+ * @throws DestinationNotFoundException If there was an issue creating the base destination, e.g.
80+ * in case of missing credentials.
81+ * @see #withBaseDestination(HttpDestination)
11482 */
11583 @ Nonnull
116- public AiCoreService withDestination ( @ Nonnull final HttpDestination destination ) {
117- baseDestinationHandler = service -> destination ;
118- return this ;
84+ public HttpDestination getBaseDestination ()
85+ throws DestinationAccessException , DestinationNotFoundException {
86+ return baseDestinationResolver . get () ;
11987 }
12088
12189 /**
122- * Set a specific deployment by id .
90+ * Get a new endpoint object, targeting a specific deployment ID .
12391 *
124- * @param deploymentId The deployment id to be used for AI Core service calls .
125- * @return A new instance of the AI Core Deployment .
92+ * @param deploymentId The deployment id to be used for the new endpoint .
93+ * @return the new instance.
12694 */
12795 @ Nonnull
128- public AiCoreDeployment forDeployment (@ Nonnull final String deploymentId ) {
129- return new AiCoreDeployment (this , () -> deploymentId );
96+ public HttpDestination getDestinationForDeploymentById (
97+ @ Nonnull final String resourceGroup , @ Nonnull final String deploymentId ) {
98+ return toInferenceDestination (resourceGroup , deploymentId );
13099 }
131100
132101 /**
133- * Set a specific deployment by model. If there are multiple deployments of the same model, the
134- * first one is returned.
102+ * Get a destination to perform inference calls for a specific model. If there are multiple
103+ * deployments of the same model, the first one is returned.
135104 *
136- * @param model The model to be used for AI Core service calls.
105+ * @param model The model to be used for inference calls.
137106 * @return A new instance of the AI Core Deployment.
138107 * @throws NoSuchElementException if no running deployment is found for the model.
139108 */
140109 @ Nonnull
141- public AiCoreDeployment forDeploymentByModel (@ Nonnull final AiModel model )
110+ public HttpDestination getDestinationForDeploymentByModel (
111+ @ Nonnull final String resourceGroup , @ Nonnull final AiModel model )
142112 throws NoSuchElementException {
143- return new AiCoreDeployment (
144- this , () -> DEPLOYMENT_CACHE . getDeploymentIdByModel ( this , resourceGroup , model ) );
113+ val deploymentId = deploymentResolver . getDeploymentIdByModel ( resourceGroup , model );
114+ return toInferenceDestination ( resourceGroup , deploymentId );
145115 }
146116
147117 /**
@@ -153,51 +123,20 @@ public AiCoreDeployment forDeploymentByModel(@Nonnull final AiModel model)
153123 * @throws NoSuchElementException if no running deployment is found for the scenario.
154124 */
155125 @ Nonnull
156- public AiCoreDeployment forDeploymentByScenario (@ Nonnull final String scenarioId )
126+ public HttpDestination getDestinationForDeploymentByScenario (
127+ @ Nonnull final String resourceGroup , @ Nonnull final String scenarioId )
157128 throws NoSuchElementException {
158- return new AiCoreDeployment (
159- this , () -> DEPLOYMENT_CACHE .getDeploymentIdByScenario (this , resourceGroup , scenarioId ));
160- }
161-
162- /**
163- * Get a destination using the default service binding loading logic.
164- *
165- * @return The destination.
166- * @throws DestinationAccessException If the destination cannot be accessed.
167- * @throws DestinationNotFoundException If the destination cannot be found.
168- */
169- @ Nonnull
170- protected HttpDestination getBaseDestination ()
171- throws DestinationAccessException , DestinationNotFoundException {
172- return destinationResolver .getDestination ();
129+ val deploymentId = deploymentResolver .getDeploymentIdByScenario (resourceGroup , scenarioId );
130+ return toInferenceDestination (resourceGroup , deploymentId );
173131 }
174132
175- /**
176- * Get the destination builder with adjustments for AI Core.
177- *
178- * @param destination The destination.
179- * @return The destination builder.
180- */
181133 @ Nonnull
182- protected DefaultHttpDestination .Builder getDestinationBuilder (
183- @ Nonnull final Destination destination ) {
184- val builder = DefaultHttpDestination .fromDestination (destination );
185- String uri = destination .get (DestinationProperty .URI ).get ();
186- if (!uri .endsWith ("/" )) {
187- uri = uri + "/" ;
188- }
189- builder .uri (uri + "v2/" ).property (AI_CLIENT_TYPE_KEY , AI_CLIENT_TYPE_VALUE );
190- return builder ;
191- }
134+ public ApiClient getApiClient () {
135+ val destination = getBaseDestination ();
136+ val httpRequestFactory = new HttpComponentsClientHttpRequestFactory ();
137+ httpRequestFactory .setHttpClient (ApacheHttpClient5Accessor .getHttpClient (destination ));
192138
193- /**
194- * Build an {@link ApiClient} that can be used for executing plain REST HTTP calls.
195- *
196- * @param destination The destination to use as basis for the client.
197- * @return The new API client.
198- */
199- @ Nonnull
200- protected ApiClient buildApiClient (@ Nonnull final Destination destination ) {
139+ val rt = new RestTemplate ();
201140 val objectMapper =
202141 new Jackson2ObjectMapperBuilder ()
203142 .modules (new JavaTimeModule ())
@@ -206,17 +145,29 @@ protected ApiClient buildApiClient(@Nonnull final Destination destination) {
206145 .serializationInclusion (JsonInclude .Include .NON_NULL ) // THIS STOPS `null` serialization
207146 .build ();
208147
209- val httpRequestFactory = new HttpComponentsClientHttpRequestFactory ();
210- httpRequestFactory .setHttpClient (ApacheHttpClient5Accessor .getHttpClient (destination ));
211-
212- val rt = new RestTemplate ();
213148 Iterables .filter (rt .getMessageConverters (), MappingJackson2HttpMessageConverter .class )
214149 .forEach (converter -> converter .setObjectMapper (objectMapper ));
215150 rt .setRequestFactory (new BufferingClientHttpRequestFactory (httpRequestFactory ));
216151
217152 return new ApiClient (rt ).setBasePath (destination .asHttp ().getUri ().toString ());
218153 }
219154
155+ private HttpDestination toInferenceDestination (
156+ @ Nonnull final String resourceGroup , @ Nonnull final String deploymentId ) {
157+ val destination = getBaseDestination ();
158+ val path = buildDeploymentPath (deploymentId );
159+
160+ return DefaultHttpDestination .fromDestination (destination )
161+ .uri (destination .getUri ().resolve (path ))
162+ .property (RESOURCE_GROUP_HEADER_PROPERTY , resourceGroup )
163+ .build ();
164+ }
165+
166+ @ Nonnull
167+ private static String buildDeploymentPath (@ Nonnull final String deploymentId ) {
168+ return "inference/deployments/%s/" .formatted (deploymentId );
169+ }
170+
220171 /**
221172 * Remove all entries from the cache then load all deployments into the cache.
222173 *
@@ -225,6 +176,6 @@ protected ApiClient buildApiClient(@Nonnull final Destination destination) {
225176 * @param resourceGroup the resource group of the deleted deployment, usually "default".
226177 */
227178 public void reloadCachedDeployments (@ Nonnull final String resourceGroup ) {
228- DEPLOYMENT_CACHE .resetCache (this , resourceGroup );
179+ new DeploymentResolver ( this ) .resetCache (resourceGroup );
229180 }
230181}
0 commit comments