From 415193ee0260678437c2a0dda5f5323ddb2ae01a Mon Sep 17 00:00:00 2001 From: Lisa Julia Nebel Date: Mon, 6 Jul 2026 13:20:11 +0200 Subject: [PATCH 01/26] Add architecture files for ai-core and recommendations, document ai already has one --- cds-feature-ai-core/docs/architecture.md | 165 ++++++++++++++++++ .../docs/architecture.md | 130 ++++++++++++++ 2 files changed, 295 insertions(+) create mode 100644 cds-feature-ai-core/docs/architecture.md create mode 100644 cds-feature-recommendations/docs/architecture.md diff --git a/cds-feature-ai-core/docs/architecture.md b/cds-feature-ai-core/docs/architecture.md new file mode 100644 index 0000000..8569c50 --- /dev/null +++ b/cds-feature-ai-core/docs/architecture.md @@ -0,0 +1,165 @@ +# Architecture: `cds-feature-ai-core` + +## Table of Contents + +- [Purpose](#purpose) +- [Dependencies](#dependencies) +- [Feature](#feature) + - [CDS Model](#cds-model) + - [Public API](#public-api) + - [Key Infrastructure Classes](#key-infrastructure-classes) + - [Multi-Tenancy](#multi-tenancy) + - [Key Flows](#key-flows) + - [Tenant Subscribe](#tenant-subscribe) + - [Tenant Unsubscribe](#tenant-unsubscribe) + - [Inference Client Resolution](#inference-client-resolution) +- [Tests](#tests) +- [Quality Tools](#quality-tools) + +--- + +## Purpose + +Bridges CAP Java to SAP AI Core's management and inference REST APIs, providing resource group management, deployment lifecycle, and inference client resolution as a standard CAP `RemoteService`. At the time of writing, `com.sap.ai.sdk:ai-core` offered no CAP integration — only raw REST API clients — so this plugin fills that gap. + +→ [README](../README.md) + +--- + +## Dependencies + +| Dependency | Why | +|---|---| +| `com.sap.ai.sdk:ai-core` (SAP AI SDK) | Provides the generated `DeploymentApi`, `ConfigurationApi`, `ResourceGroupApi`, and `ApiClient` types used to call the AI Core REST API. The plugin wraps these behind CDS events so callers never deal with the SDK directly. | +| `com.github.ben-manes.caffeine:caffeine` | Thread-safe in-process caching for `tenantId → resourceGroupId` and `resourceGroupId::configName → deploymentId` mappings (1 h TTL, 10k max per cache). | +| `io.github.resilience4j:resilience4j-retry` | Exponential backoff (initial 300 ms, doubling, max 30 s, up to 10 attempts) on 403/404/412 responses from AI Core — needed because resource group creation is eventually consistent. | +| CAP Java `DeploymentService` | MTX lifecycle hook: `AICoreSetupHandler` subscribes to `SubscribeEvent` (`@After LATE`) and `UnsubscribeEvent` (`@Before EARLY`) to create/delete per-tenant resource groups automatically. | + +--- + +## Feature + +### CDS Model + +Defined in `src/main/resources/cds/`: `AICore.cds` + +```cds +// @protocol: 'none' — programmatic access only, never exposed as OData/REST +service AICore { + @cds.persistence.skip entity resourceGroups { ... } // AI Core resource group + @cds.persistence.skip entity deployments { ... } // AI Core deployment + action stop() + @cds.persistence.skip entity configurations { ... } // AI Core configuration + + // Events: + event resourceGroup // in: (optional: tenantId — falls back to UserInfo.getTenant() from request context) → out: resourceGroupId + event deploymentId // in: resourceGroupId + ModelDeploymentSpec → out: deploymentId + event inferenceClient // in: resourceGroupId + deploymentId → out: ApiClient +} +``` + +Entities are `@cds.persistence.skip` — they have no database tables and are backed entirely by the AI Core REST API at runtime. + +--- + +### Public API + +→ [Programmatic Usage in README](../README.md#programmatic-usage) + +--- + +### Key Infrastructure Classes + +| Class | Role | +|---|---| +| `AICoreServiceConfiguration` | extends `CdsRuntimeConfiguration` — wires all handlers, clients, and caches at startup; detects AI Core binding | +| `AICoreConfig` | Immutable config record populated from `cds.ai.core.*` YAML properties | +| `AICoreClients` | Holds `DeploymentApi`, `ConfigurationApi`, `ResourceGroupApi`, and the raw `AiCoreService` from the AI SDK | +| `DeploymentResolver` | Thread-safe resolver with two Caffeine caches (`tenantId → rgId`, `rgId::configName → deploymentId`) and `ConcurrentHashMap` per-key locks (prevents duplicate deployments under concurrency); Resilience4j backoff on 403/404/412 | +| `AICoreApiHandler` | `@On` handler for the three custom events: `resourceGroup`, `deploymentId`, `inferenceClient` | +| `AICoreSetupHandler` | `@After(LATE) SubscribeEvent` / `@Before(EARLY) UnsubscribeEvent` — creates/deletes resource groups during MTX tenant lifecycle | +| `AbstractCrudHandler` | Base for all entity CRUD handlers; provides `resolveResourceGroup()` and `ensureResourceGroupAccessible()` (tenant isolation guard) | + +--- + +### Multi-Tenancy + +→ [Multi-Tenancy in README](../README.md#multi-tenancy) + +--- + +### Key Flows + +#### Tenant Subscribe + +``` +CAP MTX DeploymentService + | + | SubscribeEvent @After(LATE) + v +AICoreSetupHandler + | + | resolveResourceGroup(tenantId) + v +DeploymentResolver + | + | GET /v2/admin/resourceGroups?labelFilter=CDS_TENANT_ID=tenantId + v +SAP AI Core + | + | (if absent) POST /v2/admin/resourceGroups + v +resourceGroupId (cached 1h after last access — subsequent calls skip the AI Core management API; + if a resource group is deleted or reassigned externally, the plugin won't notice until the cache expires after 1h or the app restarts) +``` + +#### Inference Client Resolution + +```mermaid +flowchart TD + A["aiCoreService.inferenceClient(rgId, deploymentId)"] --> B["DeploymentResolver: check tenantResourceGroupCache"] + B -->|cache hit| E["check deploymentCache"] + B -->|cache miss| C["GET /v2/admin/resourceGroups — find by tenantId label"] + C --> D["PUT in tenantResourceGroupCache (1h TTL)"] + D --> E + E -->|cache hit| V["validateCachedDeployment: GET /v2/lm/deployments/{id}"] + V -->|valid| H["emit InferenceClientContext → ApiClient"] + V -->|invalid — invalidate cache entry| F + E -->|cache miss| F["GET /v2/lm/deployments — match by ModelDeploymentSpec"] + F -->|not found| G["POST /v2/lm/configurations + POST /v2/lm/deployments — poll until RUNNING"] + F -->|found| I["PUT in deploymentCache (1h TTL)"] + G --> I + I --> H +``` +*Resilience4j exponential backoff (300 ms initial, doubling, capped at 30 s, max 10 attempts) on: 403/412 during deployment creation (`POST /v2/lm/deployments`); 403/404/412 during deployment polling (`GET /v2/lm/deployments`).* + + +#### Tenant Unsubscribe + +``` +CAP MTX DeploymentService + | + | UnsubscribeEvent @Before(EARLY) + v +AICoreSetupHandler + | + | DELETE /v2/admin/resourceGroups/{id} + v +SAP AI Core + | + | invalidateTenant(tenantId) — evicts tenantResourceGroupCache and deploymentCache (which was filled on first call to resolveDeployment) entries for this tenant + v +(done) +``` +--- + +## Tests + +Unit tests for `AICoreServiceConfiguration`, `AICoreServiceImpl`, `AICoreSetupHandler`, and the CRUD handlers live in `src/test/` within this module (`mvn test`). + +End-to-end integration tests against a real AI Core instance live in [`integration-tests/spring/`](../../integration-tests/README.md) (`AICoreServiceTest`, `DeploymentTest`, `ResourceGroupTest`, `MultiTenancyTest`, and others). MTX lifecycle tests (subscribe/unsubscribe/tenant isolation) live in [`integration-tests/mtx-local/`](../../integration-tests/README.md). + +--- + +## Quality Tools + +→ [CI Checks and static analysis of outer module](../../README.md#ci-checks) diff --git a/cds-feature-recommendations/docs/architecture.md b/cds-feature-recommendations/docs/architecture.md new file mode 100644 index 0000000..e6e1b62 --- /dev/null +++ b/cds-feature-recommendations/docs/architecture.md @@ -0,0 +1,130 @@ +# Architecture: `cds-feature-recommendations` + +## Table of Contents + +- [Purpose](#purpose) +- [Dependencies](#dependencies) +- [Feature](#feature) + - [CDS Model](#cds-model) + - [Configuration](#configuration) + - [Public API / Handlers](#public-api--handlers) + - [Key Infrastructure Classes](#key-infrastructure-classes) + - [Multi-Tenancy](#multi-tenancy) + - [Key Flows](#key-flows) + - [Recommendation Pipeline (OData GET on draft entity)](#recommendation-pipeline-odata-get-on-draft-entity) + - [MTX Model Change — Cache Invalidation](#mtx-model-change--cache-invalidation) +- [Tests](#tests) +- [Quality Tools](#quality-tools) + +--- + +## Purpose + +Automatically injects AI-powered field recommendations from the SAP RPT-1 tabular prediction foundation model into Fiori Elements OData responses for draft-enabled entities. Zero application code required. + +→ [README](../README.md) + +--- + +## Dependencies + +| Dependency | Why | +|---|---| +| [`cds-feature-ai-core`](../../cds-feature-ai-core/README.md) | Provides the `AICore` CDS service and `AICoreService` API used to resolve the resource group, deployment ID, and inference `ApiClient` for the RPT-1 model. Recommendations cannot function without an active AI Core connection. | +| `@cap-js/ai` (Node.js CDS plugin) | At CDS build time, the plugin adds the `SAP_Recommendations` navigation property to draft-enabled entities that have value-list fields. Without this (or a manual CDS extension), predictions are computed but not serialized in OData responses. | +| CAP Java `ExtensibilityService` | `RecommendationModelChangedHandler` listens to `EVENT_MODEL_CHANGED` to invalidate the per-tenant entity cache when a tenant's CDS model is upgraded via MTX. | + +--- + +## Feature + +### CDS Model + +No dedicated CDS model file — the plugin relies on the `AICore` service model provided by `cds-feature-ai-core`, and on the `SAP_Recommendations` navigation property injected by the `@cap-js/ai` Node.js plugin (or added manually by the application). + +The Node plugin will automatically detect fields annotated with a value list, see [`README`](../README.md#enabling-recommendations). + +### Configuration + +Wired by `RecommendationConfiguration` (extends `CdsRuntimeConfiguration`) at startup. It detects whether an AI Core binding is present and selects production vs. mock mode accordingly — no manual activation is required. + +### Public API / Handlers + +No Java API — the plugin is entirely annotation-driven. Extend or annotate your CDS model to control which fields receive recommendations, see [`README`](../README.md#enabling-recommendations). +Currently, it is not possible to hook into the recommendation result from application code to observe the injected output, nor to override the inference call itself ([#110](https://github.com/cap-java/cds-ai/issues/110)). + +### Key Infrastructure Classes + +| Class | Role | +|---|---| +| `RecommendationConfiguration` | extends `CdsRuntimeConfiguration` — wires all handlers at startup; selects production vs. mock based on AI Core binding presence | +| `FioriRecommendationHandler` | `@After` read handler on all app services (`entity="*"`) — cross-cutting read interceptor; orchestrates the full recommendation pipeline | +| `RecommendationContextBuilder` | Reads CDS annotations to determine which fields are prediction targets and which columns supply training context | +| `RptModelSpec` | Static factory for the `ModelDeploymentSpec` targeting `sap-rpt-1-small`; used as the cache key for deployment resolution | +| `RptInferenceClient` | Calls RPT-1 `/predict` endpoint; handles the synthetic `SAP_RECOMMENDATIONS_ID` index column for composite/non-string keys | +| `RecommendationResultParser` | Type-coerces RPT-1 string output back to CDS primitive types; resolves `@Common.Text` descriptions from the database | +| `RecommendationModelChangedHandler` | `@On(EVENT_MODEL_CHANGED)` — invalidates per-tenant entity cache on MTX model upgrade | + +### Multi-Tenancy + +→ [Multi-Tenancy in cds-feature-ai-core README](../../cds-feature-ai-core/README.md#multi-tenancy) + +Tenant isolation is inherited from `cds-feature-ai-core`: each prediction call resolves the resource group and deployment for the current request's tenant. No additional MT configuration is required in this module. + +**Per-tenant entity cache** (in `FioriRecommendationHandler`): + +``` +Cache<":", Boolean> 10k max, no TTL + → entities with no prediction columns are recorded and skipped on every future read + → invalidated by RecommendationModelChangedHandler on model change +``` + +### Key Flows + +#### Recommendation Pipeline (OData GET on draft entity) + +```mermaid +flowchart TD + A["OData GET — IsActiveEntity=false"] --> B["FioriRecommendationHandler @After(entity='*') afterRead(...)"] + B --> C{Entity in no-prediction cache?} + C -->|yes — skip| Z["Return response unchanged"] + C -->|no| D{Draft row? Single result?} + D -->|no| Z + D -->|yes| E["RecommendationContextBuilder: identify prediction fields + context columns"] + E --> F{Does this entity have any prediction fields?} + F -->|no — add entity to skip cache| Z + F -->|yes| G["DB query: up to 2000 context rows (ORDER BY modifiedAt DESC)"] + G --> H["cds-feature-ai-core: resolveResourceGroup → resolveDeploymentId → inferenceClient"] + H --> I["RptInferenceClient.predict(predictRow, contextRows, columns) POST /v2/inference/deployments/{id}/predict"] + I --> J["RecommendationResultParser: type-convert + resolve @Common.Text descriptions"] + J --> K["Inject SAP_Recommendations into response row"] + K --> L["Return enriched response"] +``` + +#### No-prediction-cache Invalidation + +``` +ExtensibilityService + | + | EVENT_MODEL_CHANGED (tenantId) + v +RecommendationModelChangedHandler + | + | evict all entries in no-prediction-cache for tenantId + v +Next read re-evaluates +``` + +--- + +## Tests + +Unit tests for `FioriRecommendationHandler`, `RptInferenceClient`, and `RecommendationConfiguration` live in `src/test/` within this module (`mvn test`). + +End-to-end integration tests covering the full recommendation pipeline against a real AI Core instance live in [`integration-tests/`](../../integration-tests/README.md) in the outer module (`RecommendationTest`, `NonStandardKeyRecommendationTest`). + +--- + +## Quality Tools + +→ [CI Checks and static analysis of outer module](../../README.md#ci-checks) From 95b1fc222e032bb970caa0a934a20bd1b9f03e2a Mon Sep 17 00:00:00 2001 From: Lisa Julia Nebel Date: Wed, 8 Jul 2026 11:09:18 +0200 Subject: [PATCH 02/26] Update cds-feature-ai-core/docs/architecture.md Co-authored-by: Marvin L Signed-off-by: Lisa Julia Nebel --- cds-feature-ai-core/docs/architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cds-feature-ai-core/docs/architecture.md b/cds-feature-ai-core/docs/architecture.md index 8569c50..e420c68 100644 --- a/cds-feature-ai-core/docs/architecture.md +++ b/cds-feature-ai-core/docs/architecture.md @@ -32,7 +32,7 @@ Bridges CAP Java to SAP AI Core's management and inference REST APIs, providing |---|---| | `com.sap.ai.sdk:ai-core` (SAP AI SDK) | Provides the generated `DeploymentApi`, `ConfigurationApi`, `ResourceGroupApi`, and `ApiClient` types used to call the AI Core REST API. The plugin wraps these behind CDS events so callers never deal with the SDK directly. | | `com.github.ben-manes.caffeine:caffeine` | Thread-safe in-process caching for `tenantId → resourceGroupId` and `resourceGroupId::configName → deploymentId` mappings (1 h TTL, 10k max per cache). | -| `io.github.resilience4j:resilience4j-retry` | Exponential backoff (initial 300 ms, doubling, max 30 s, up to 10 attempts) on 403/404/412 responses from AI Core — needed because resource group creation is eventually consistent. | +| `io.github.resilience4j:resilience4j-retry` | Exponential backoff (initial 300 ms, doubling, max 30 s, up to 10 attempts) on 403/404/412 responses from AI Core - needed because resource group creation is asyncronous. | | CAP Java `DeploymentService` | MTX lifecycle hook: `AICoreSetupHandler` subscribes to `SubscribeEvent` (`@After LATE`) and `UnsubscribeEvent` (`@Before EARLY`) to create/delete per-tenant resource groups automatically. | --- From 1947f56b3bdba66f29d0e11ec3dd2757ac097deb Mon Sep 17 00:00:00 2001 From: Lisa Julia Nebel Date: Thu, 9 Jul 2026 14:45:10 +0200 Subject: [PATCH 03/26] Incorporate Marvins comments --- cds-feature-ai-core/docs/architecture.md | 65 +++++++++++++++---- .../docs/architecture.md | 4 +- 2 files changed, 54 insertions(+), 15 deletions(-) diff --git a/cds-feature-ai-core/docs/architecture.md b/cds-feature-ai-core/docs/architecture.md index e420c68..93464c4 100644 --- a/cds-feature-ai-core/docs/architecture.md +++ b/cds-feature-ai-core/docs/architecture.md @@ -33,7 +33,7 @@ Bridges CAP Java to SAP AI Core's management and inference REST APIs, providing | `com.sap.ai.sdk:ai-core` (SAP AI SDK) | Provides the generated `DeploymentApi`, `ConfigurationApi`, `ResourceGroupApi`, and `ApiClient` types used to call the AI Core REST API. The plugin wraps these behind CDS events so callers never deal with the SDK directly. | | `com.github.ben-manes.caffeine:caffeine` | Thread-safe in-process caching for `tenantId → resourceGroupId` and `resourceGroupId::configName → deploymentId` mappings (1 h TTL, 10k max per cache). | | `io.github.resilience4j:resilience4j-retry` | Exponential backoff (initial 300 ms, doubling, max 30 s, up to 10 attempts) on 403/404/412 responses from AI Core - needed because resource group creation is asyncronous. | -| CAP Java `DeploymentService` | MTX lifecycle hook: `AICoreSetupHandler` subscribes to `SubscribeEvent` (`@After LATE`) and `UnsubscribeEvent` (`@Before EARLY`) to create/delete per-tenant resource groups automatically. | +| `com.sap.cds:cds-services-api/-impl/-utils` | CAP Java integration — used to integrate the plugin into the CAP runtime. | --- @@ -114,24 +114,61 @@ resourceGroupId (cached 1h after last access — subsequent calls skip the AI Co #### Inference Client Resolution +##### Event 1: resourceGroup + +```mermaid +flowchart TD + A1["emit ResourceGroupContext (tenantId)"] + A1 --> A2{"multiTenancy enabled
AND tenantId != null?"} + A2 -->|no| A3["return config.defaultResourceGroup()"] + A2 -->|yes| A4{"tenantResourceGroupCache
lookup by tenantId"} + A4 -->|cache hit| A5["return cached resourceGroupId"] + A4 -->|cache miss| A6["GET /v2/admin/resourceGroups
labelSelector: ext.ai.sap.com/tenant={tenantId}"] + A6 --> A7{"found?"} + A7 -->|yes| A8["cache result (expireAfterAccess 1h)"] + A7 -->|no| A9["POST /v2/admin/resourceGroups
(handle 409 Conflict = already exists)"] + A9 --> A8 + A8 --> A5 +``` + +##### Event 2: deploymentId — invoked with `resourceGroupId` + ```mermaid flowchart TD - A["aiCoreService.inferenceClient(rgId, deploymentId)"] --> B["DeploymentResolver: check tenantResourceGroupCache"] - B -->|cache hit| E["check deploymentCache"] - B -->|cache miss| C["GET /v2/admin/resourceGroups — find by tenantId label"] - C --> D["PUT in tenantResourceGroupCache (1h TTL)"] - D --> E - E -->|cache hit| V["validateCachedDeployment: GET /v2/lm/deployments/{id}"] - V -->|valid| H["emit InferenceClientContext → ApiClient"] - V -->|invalid — invalidate cache entry| F - E -->|cache miss| F["GET /v2/lm/deployments — match by ModelDeploymentSpec"] - F -->|not found| G["POST /v2/lm/configurations + POST /v2/lm/deployments — poll until RUNNING"] - F -->|found| I["PUT in deploymentCache (1h TTL)"] - G --> I - I --> H + B1["emit DeploymentIdContext
(resourceGroupId, ModelDeploymentSpec)"] + B1 --> B2["acquire per-key lock
(ConcurrentHashMap)"] + B2 --> B3{"deploymentCache
lookup by rgId::configName"} + B3 -->|cache hit| B4["validateCachedDeployment:
GET /v2/lm/deployments/{id}"] + B4 --> B5{"status RUNNING or PENDING?"} + B5 -->|yes| B6["return cached deploymentId"] + B5 -->|no / 404| B7["invalidate cache entry"] + B7 --> B8 + B3 -->|cache miss| B8["findOrCreateDeployment (under lock)"] + B8 --> B9["queryDeploymentsUntilReady (with retry):
GET /v2/lm/deployments?scenarioId=..."] + B9 --> B10{"match by configName
+ matchesExisting() + RUNNING/PENDING?"} + B10 -->|found| B11["cache deploymentId (expireAfterAccess 1h)"] + B10 -->|not found| B12["findOrCreateConfiguration:
GET /v2/lm/configurations?scenarioId=..."] + B12 --> B13{"config with matching name exists?"} + B13 -->|yes| B14["reuse existing configId"] + B13 -->|no| B15["POST /v2/lm/configurations"] + B15 --> B14 + B14 --> B16["POST /v2/lm/deployments (with retry for 403/412)"] + B16 --> B17["pollUntilRunning:
GET /v2/lm/deployments/{id}
(exponential backoff)"] + B17 --> B11 + B11 --> B6 ``` + *Resilience4j exponential backoff (300 ms initial, doubling, capped at 30 s, max 10 attempts) on: 403/412 during deployment creation (`POST /v2/lm/deployments`); 403/404/412 during deployment polling (`GET /v2/lm/deployments`).* +##### Event 3: inferenceClient — invoked with `resourceGroupId` and `deploymentId` + +```mermaid +flowchart TD + C1["emit InferenceClientContext
(resourceGroupId, deploymentId)"] + C1 --> C2["clients.sdkService()
.getInferenceDestination(rgId)
.usingDeploymentId(depId)"] + C2 --> C3["return ApiClient.create(destination)"] +``` + #### Tenant Unsubscribe diff --git a/cds-feature-recommendations/docs/architecture.md b/cds-feature-recommendations/docs/architecture.md index e6e1b62..b883f01 100644 --- a/cds-feature-recommendations/docs/architecture.md +++ b/cds-feature-recommendations/docs/architecture.md @@ -32,7 +32,9 @@ Automatically injects AI-powered field recommendations from the SAP RPT-1 tabula |---|---| | [`cds-feature-ai-core`](../../cds-feature-ai-core/README.md) | Provides the `AICore` CDS service and `AICoreService` API used to resolve the resource group, deployment ID, and inference `ApiClient` for the RPT-1 model. Recommendations cannot function without an active AI Core connection. | | `@cap-js/ai` (Node.js CDS plugin) | At CDS build time, the plugin adds the `SAP_Recommendations` navigation property to draft-enabled entities that have value-list fields. Without this (or a manual CDS extension), predictions are computed but not serialized in OData responses. | -| CAP Java `ExtensibilityService` | `RecommendationModelChangedHandler` listens to `EVENT_MODEL_CHANGED` to invalidate the per-tenant entity cache when a tenant's CDS model is upgraded via MTX. | +| `com.sap.ai.sdk.foundationmodels:sap-rpt` (SAP AI SDK) | Provides the RPT-1 model client used to call the `/predict` endpoint. | +| `com.github.ben-manes.caffeine:caffeine` | Thread-safe in-process caching for the per-tenant entity skip cache (10k max, no TTL). | +| `com.sap.cds:cds-services-api/-impl/-utils` | CAP Java integration — used to integrate the plugin into the CAP runtime. | --- From 5b60823e7a7a6b081cd2810b863962836eb3d699 Mon Sep 17 00:00:00 2001 From: Lisa Julia Nebel Date: Thu, 9 Jul 2026 14:46:19 +0200 Subject: [PATCH 04/26] Update links to CI checks --- cds-feature-ai-core/docs/architecture.md | 2 +- cds-feature-recommendations/docs/architecture.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cds-feature-ai-core/docs/architecture.md b/cds-feature-ai-core/docs/architecture.md index 93464c4..13cd192 100644 --- a/cds-feature-ai-core/docs/architecture.md +++ b/cds-feature-ai-core/docs/architecture.md @@ -199,4 +199,4 @@ End-to-end integration tests against a real AI Core instance live in [`integrati ## Quality Tools -→ [CI Checks and static analysis of outer module](../../README.md#ci-checks) +→ [CI Checks and static analysis](../../CONTRIBUTING.md#ci-checks) diff --git a/cds-feature-recommendations/docs/architecture.md b/cds-feature-recommendations/docs/architecture.md index b883f01..b740d57 100644 --- a/cds-feature-recommendations/docs/architecture.md +++ b/cds-feature-recommendations/docs/architecture.md @@ -129,4 +129,4 @@ End-to-end integration tests covering the full recommendation pipeline against a ## Quality Tools -→ [CI Checks and static analysis of outer module](../../README.md#ci-checks) +→ [CI Checks and static analysis](../../CONTRIBUTING.md#ci-checks) From a4a982bf3bb40f21948f9545958cf17423c47aaa Mon Sep 17 00:00:00 2001 From: Lisa Julia Nebel Date: Thu, 6 Aug 2026 18:06:58 +0200 Subject: [PATCH 05/26] Incorporate Adrians comments --- cds-feature-ai-core/docs/architecture.md | 119 +++++++++++------- .../docs/architecture.md | 85 ++++++++----- 2 files changed, 129 insertions(+), 75 deletions(-) diff --git a/cds-feature-ai-core/docs/architecture.md b/cds-feature-ai-core/docs/architecture.md index 13cd192..bf69c96 100644 --- a/cds-feature-ai-core/docs/architecture.md +++ b/cds-feature-ai-core/docs/architecture.md @@ -20,7 +20,7 @@ ## Purpose -Bridges CAP Java to SAP AI Core's management and inference REST APIs, providing resource group management, deployment lifecycle, and inference client resolution as a standard CAP `RemoteService`. At the time of writing, `com.sap.ai.sdk:ai-core` offered no CAP integration — only raw REST API clients — so this plugin fills that gap. +Bridges CAP Java to SAP AI Core's management and inference REST APIs, providing resource group management, deployment lifecycle, and inference client resolution as a CAP service. At the time of writing, `com.sap.ai.sdk:ai-core` offered no CAP integration — only raw REST API clients — so this plugin fills that gap. → [README](../README.md) @@ -30,7 +30,7 @@ Bridges CAP Java to SAP AI Core's management and inference REST APIs, providing | Dependency | Why | |---|---| -| `com.sap.ai.sdk:ai-core` (SAP AI SDK) | Provides the generated `DeploymentApi`, `ConfigurationApi`, `ResourceGroupApi`, and `ApiClient` types used to call the AI Core REST API. The plugin wraps these behind CDS events so callers never deal with the SDK directly. | +| `com.sap.ai.sdk:ai-core` (SAP AI SDK) | Provides the generated `DeploymentApi`, `ConfigurationApi`, `ResourceGroupApi`, and `ApiClient` types used to call the AI Core REST API. The plugin wraps these behind CDS events so callers never deal with the AI SDK directly. | | `com.github.ben-manes.caffeine:caffeine` | Thread-safe in-process caching for `tenantId → resourceGroupId` and `resourceGroupId::configName → deploymentId` mappings (1 h TTL, 10k max per cache). | | `io.github.resilience4j:resilience4j-retry` | Exponential backoff (initial 300 ms, doubling, max 30 s, up to 10 attempts) on 403/404/412 responses from AI Core - needed because resource group creation is asyncronous. | | `com.sap.cds:cds-services-api/-impl/-utils` | CAP Java integration — used to integrate the plugin into the CAP runtime. | @@ -49,16 +49,19 @@ service AICore { @cds.persistence.skip entity resourceGroups { ... } // AI Core resource group @cds.persistence.skip entity deployments { ... } // AI Core deployment + action stop() @cds.persistence.skip entity configurations { ... } // AI Core configuration - - // Events: - event resourceGroup // in: (optional: tenantId — falls back to UserInfo.getTenant() from request context) → out: resourceGroupId - event deploymentId // in: resourceGroupId + ModelDeploymentSpec → out: deploymentId - event inferenceClient // in: resourceGroupId + deploymentId → out: ApiClient } ``` Entities are `@cds.persistence.skip` — they have no database tables and are backed entirely by the AI Core REST API at runtime. +The three custom events are **not declared in the CDS model** — they are Java-only `@EventName`-annotated `EventContext` interfaces in the `api` package: + +| Java interface | Event name | In → Out | +|---|---|---| +| `ResourceGroupContext` | `resourceGroup` | `tenantId?` → `resourceGroupId` | +| `DeploymentIdContext` | `deploymentId` | `resourceGroupId + ModelDeploymentSpec` → `deploymentId` | +| `InferenceClientContext` | `inferenceClient` | `resourceGroupId + deploymentId` → `ApiClient` | + --- ### Public API @@ -114,62 +117,84 @@ resourceGroupId (cached 1h after last access — subsequent calls skip the AI Co #### Inference Client Resolution +Every prediction request or inference call requires a fully-configured `ApiClient` that is scoped to a specific AI Core deployment. The challenge is that creating this client requires three sequential steps +— tenant → resource group +- resource group + model spec (e.g. RPT-1) → deployment +- deployment → ApiClient +Each of these involves a remote API call to AI Core (resource groups and deployments are created asynchronously and may not be immediately available). The plugin solves this with per-step Caffeine caches (1 h TTL) to avoid redundant AI Core API calls, `ConcurrentHashMap` per-key locks to prevent duplicate deployment creation under concurrent requests, and Resilience4j exponential backoff to handle the window between a resource group or deployment being created and it becoming usable. + +The three steps map directly to the three `EventContext` interfaces and are executed in sequence by `AICoreServiceImpl`: + ##### Event 1: resourceGroup +Emitted by any caller (e.g. `cds-feature-recommendations`) via `AICoreService.resourceGroup()` to resolve the AI Core resource group ID for the current tenant. In single-tenant mode, the configured default resource group is returned directly. In multi-tenant mode, `DeploymentResolver` looks up the resource group by the `ext.ai.sap.com/CDS_TENANT_ID` label with a `GET` to `/v2/admin/resourceGroups`. The result is cached for 1 h (expire-after-access) so the AI Core management API is not called on every request. If no resource group is found, it creates one via `POST` to `/v2/admin/resourceGroups` (tolerating 409 conflicts), caches the result, and returns the resource group ID. +Unlike the deployment cache, there is **no validation on cache hits** — if a resource group is deleted externally during that window, the stale ID stays cached and subsequent prediction requests will fail until the entry expires or the app restarts. + + +##### Event 2: deploymentId — invoked with `resourceGroupId` + +Emitted by callers via `AICoreService.deploymentId(rgId, spec)` to resolve (or lazily create) a running deployment matching the given `ModelDeploymentSpec` inside the resource group. `DeploymentResolver` first checks its deployment cache; on a cache miss or invalid cached entry it queries AI Core for an existing RUNNING/PENDING deployment, and — if none exists — creates the configuration and deployment, then polls until RUNNING with exponential backoff: Resilience4j exponential backoff (300 ms initial, doubling, capped at 30 s, max 10 attempts) on: 403/412 during deployment creation (`POST /v2/lm/deployments`); 403/404/412 during deployment polling (`GET /v2/lm/deployments`). A `ConcurrentHashMap` per-key lock prevents duplicate deployments from being created under concurrent first-use requests. The resolved deployment ID is cached for 1 h. + + +##### Event 3: inferenceClient — invoked with `resourceGroupId` and `deploymentId` + +Emitted by callers via `AICoreService.inferenceClient(rgId, deploymentId)` to obtain a pre-configured `ApiClient` ready to make prediction requests against a specific deployment. The handler delegates to the SAP AI SDK's `AiCoreService` to build an inference destination scoped to the resource group and deployment, then wraps it in an `ApiClient`. No caching — the client is lightweight to construct and callers are expected to obtain it once per request. + +#### Full flow + ```mermaid +%%{init: {'theme': 'base', 'themeVariables': {'edgeLabelBackground': '#ffffff00', 'fontSize': '14px', 'primaryColor': '#fff', 'primaryBorderColor': '#000', 'primaryTextColor': '#000'}}}%% flowchart TD - A1["emit ResourceGroupContext (tenantId)"] - A1 --> A2{"multiTenancy enabled
AND tenantId != null?"} - A2 -->|no| A3["return config.defaultResourceGroup()"] - A2 -->|yes| A4{"tenantResourceGroupCache
lookup by tenantId"} - A4 -->|cache hit| A5["return cached resourceGroupId"] - A4 -->|cache miss| A6["GET /v2/admin/resourceGroups
labelSelector: ext.ai.sap.com/tenant={tenantId}"] - A6 --> A7{"found?"} - A7 -->|yes| A8["cache result (expireAfterAccess 1h)"] - A7 -->|no| A9["POST /v2/admin/resourceGroups
(handle 409 Conflict = already exists)"] + classDef process fill:#fff,stroke:#000,color:#000 + classDef skip fill:#f4f4f4,stroke:#000,color:#000 + + START@{ shape: sm-circ, label: "start" } --> A(["Consumer: getAICoreService() → RemoteService"]):::process + + A --> A1(["emit ResourceGroupContext (tenantId)"]):::process + A1 --> A2{multiTenancy enabled AND tenantId != null?} + A2 -->|no| A3(["return config.defaultResourceGroup()"]):::skip + A2 -->|yes| A4{tenantResourceGroupCache hit?} + A4 -->|yes| A5(["return cached resourceGroupId"]):::skip + A4 -->|no| A6(["GET /v2/admin/resourceGroups?labelSelector=...tenant={tenantId}"]):::process + A6 --> A7{found?} + A7 -->|yes| A8(["cache result (expireAfterAccess 1h)"]):::process + A7 -->|no| A9(["POST /v2/admin/resourceGroups (409 = already exists → ok)"]):::process A9 --> A8 A8 --> A5 -``` -##### Event 2: deploymentId — invoked with `resourceGroupId` + A3 --> B1 + A5 --> B1 -```mermaid -flowchart TD - B1["emit DeploymentIdContext
(resourceGroupId, ModelDeploymentSpec)"] - B1 --> B2["acquire per-key lock
(ConcurrentHashMap)"] - B2 --> B3{"deploymentCache
lookup by rgId::configName"} - B3 -->|cache hit| B4["validateCachedDeployment:
GET /v2/lm/deployments/{id}"] - B4 --> B5{"status RUNNING or PENDING?"} - B5 -->|yes| B6["return cached deploymentId"] - B5 -->|no / 404| B7["invalidate cache entry"] + B1(["emit DeploymentIdContext (resourceGroupId, ModelDeploymentSpec)"]):::process + B1 --> B2(["acquire per-key lock (ConcurrentHashMap)"]):::process + B2 --> B3{deploymentCache hit?} + B3 -->|yes| B4(["GET /v2/lm/deployments/{id} — validate cached entry"]):::process + B4 --> B5{status RUNNING or PENDING?} + B5 -->|yes| B6(["return cached deploymentId"]):::skip + B5 -->|no / 404| B7(["invalidate cache entry"]):::process B7 --> B8 - B3 -->|cache miss| B8["findOrCreateDeployment (under lock)"] - B8 --> B9["queryDeploymentsUntilReady (with retry):
GET /v2/lm/deployments?scenarioId=..."] - B9 --> B10{"match by configName
+ matchesExisting() + RUNNING/PENDING?"} - B10 -->|found| B11["cache deploymentId (expireAfterAccess 1h)"] - B10 -->|not found| B12["findOrCreateConfiguration:
GET /v2/lm/configurations?scenarioId=..."] - B12 --> B13{"config with matching name exists?"} - B13 -->|yes| B14["reuse existing configId"] - B13 -->|no| B15["POST /v2/lm/configurations"] + B3 -->|no| B8(["GET /v2/lm/deployments?scenarioId=... — query existing deployments"]):::process + B8 --> B10{match by configName + RUNNING/PENDING?} + B10 -->|yes| B11(["cache deploymentId (expireAfterAccess 1h)"]):::process + B10 -->|no| B12(["GET /v2/lm/configurations?scenarioId=... — find or create config"]):::process + B12 --> B13{config with matching name exists?} + B13 -->|yes| B14(["reuse existing configId"]):::process + B13 -->|no| B15(["POST /v2/lm/configurations"]):::process B15 --> B14 - B14 --> B16["POST /v2/lm/deployments (with retry for 403/412)"] - B16 --> B17["pollUntilRunning:
GET /v2/lm/deployments/{id}
(exponential backoff)"] + B14 --> B16(["POST /v2/lm/deployments (retry on 403/412)"]):::process + B16 --> B17(["poll GET /v2/lm/deployments/{id} until RUNNING (exponential backoff)"]):::process B17 --> B11 B11 --> B6 -``` - -*Resilience4j exponential backoff (300 ms initial, doubling, capped at 30 s, max 10 attempts) on: 403/412 during deployment creation (`POST /v2/lm/deployments`); 403/404/412 during deployment polling (`GET /v2/lm/deployments`).* -##### Event 3: inferenceClient — invoked with `resourceGroupId` and `deploymentId` + B6 --> C1(["emit InferenceClientContext (resourceGroupId, deploymentId)"]):::process + C1 --> C2(["getInferenceDestination(rgId).usingDeploymentId(depId)"]):::process + C2 --> C3(["return ApiClient.create(destination)"]):::process + C3 --> END1@{ shape: framed-circle, label: "stop" } -```mermaid -flowchart TD - C1["emit InferenceClientContext
(resourceGroupId, deploymentId)"] - C1 --> C2["clients.sdkService()
.getInferenceDestination(rgId)
.usingDeploymentId(depId)"] - C2 --> C3["return ApiClient.create(destination)"] + style START fill:#000,stroke:#000,color:#000 + style END1 fill:#000,stroke:#000,stroke-width:3px,color:#000 ``` - #### Tenant Unsubscribe ``` diff --git a/cds-feature-recommendations/docs/architecture.md b/cds-feature-recommendations/docs/architecture.md index b740d57..8287bca 100644 --- a/cds-feature-recommendations/docs/architecture.md +++ b/cds-feature-recommendations/docs/architecture.md @@ -12,6 +12,7 @@ - [Multi-Tenancy](#multi-tenancy) - [Key Flows](#key-flows) - [Recommendation Pipeline (OData GET on draft entity)](#recommendation-pipeline-odata-get-on-draft-entity) + - [Context row selection](#context-row-selection) - [MTX Model Change — Cache Invalidation](#mtx-model-change--cache-invalidation) - [Tests](#tests) - [Quality Tools](#quality-tools) @@ -20,7 +21,7 @@ ## Purpose -Automatically injects AI-powered field recommendations from the SAP RPT-1 tabular prediction foundation model into Fiori Elements OData responses for draft-enabled entities. Zero application code required. +Automatically injects AI-powered field recommendations from the [SAP RPT-1](https://help.sap.com/docs/sap-ai-core/generative-ai/sap-rpt-1) tabular prediction foundation model into Fiori Elements OData responses for draft-enabled entities. Zero application code required. → [README](../README.md) @@ -33,7 +34,7 @@ Automatically injects AI-powered field recommendations from the SAP RPT-1 tabula | [`cds-feature-ai-core`](../../cds-feature-ai-core/README.md) | Provides the `AICore` CDS service and `AICoreService` API used to resolve the resource group, deployment ID, and inference `ApiClient` for the RPT-1 model. Recommendations cannot function without an active AI Core connection. | | `@cap-js/ai` (Node.js CDS plugin) | At CDS build time, the plugin adds the `SAP_Recommendations` navigation property to draft-enabled entities that have value-list fields. Without this (or a manual CDS extension), predictions are computed but not serialized in OData responses. | | `com.sap.ai.sdk.foundationmodels:sap-rpt` (SAP AI SDK) | Provides the RPT-1 model client used to call the `/predict` endpoint. | -| `com.github.ben-manes.caffeine:caffeine` | Thread-safe in-process caching for the per-tenant entity skip cache (10k max, no TTL). | +| `com.github.ben-manes.caffeine:caffeine` | Thread-safe in-process caching for the per-tenant entity skip cache (10k max, no TTL). This might be replaced in #129.| | `com.sap.cds:cds-services-api/-impl/-utils` | CAP Java integration — used to integrate the plugin into the CAP runtime. | --- @@ -44,7 +45,7 @@ Automatically injects AI-powered field recommendations from the SAP RPT-1 tabula No dedicated CDS model file — the plugin relies on the `AICore` service model provided by `cds-feature-ai-core`, and on the `SAP_Recommendations` navigation property injected by the `@cap-js/ai` Node.js plugin (or added manually by the application). -The Node plugin will automatically detect fields annotated with a value list, see [`README`](../README.md#enabling-recommendations). +The Node plugin will automatically detect fields annotated with a value list, i.e., fields annotated with `@Common.ValueList`, `@Common.ValueListWithFixedValues`, or whose association target has `@cds.odata.valuelist`, also see [`README`](../README.md#enabling-recommendations). ### Configuration @@ -81,40 +82,68 @@ Cache<":", Boolean> 10k max, no TTL → invalidated by RecommendationModelChangedHandler on model change ``` +The cache is keyed by `:` and is invalidated on `ExtensibilityService.EVENT_MODEL_CHANGED` — ensuring that model upgrades (which may add or remove value-list annotations) are reflected without a restart, see also [MTX Model Change — Cache Invalidation](#mtx-model-change--cache-invalidation). This cache might be replaced with #129. + +Currently the cache stores only **misses** — entities that have no prediction columns. For entities that *do* have prediction columns, `RecommendationContextBuilder` re-derives them from the CDS model on every request. An alternative design would cache `Set` (the prediction column names) instead of `Boolean`, using an empty set for the no-prediction case. This would eliminate the per-request model scan for all entities, at the cost of a slightly larger cache value. + ### Key Flows #### Recommendation Pipeline (OData GET on draft entity) ```mermaid +%%{init: {'theme': 'base', 'themeVariables': {'edgeLabelBackground': '#ffffff00', 'fontSize': '14px', 'primaryColor': '#fff', 'primaryBorderColor': '#000', 'primaryTextColor': '#000'}}}%% flowchart TD - A["OData GET — IsActiveEntity=false"] --> B["FioriRecommendationHandler @After(entity='*') afterRead(...)"] - B --> C{Entity in no-prediction cache?} - C -->|yes — skip| Z["Return response unchanged"] - C -->|no| D{Draft row? Single result?} - D -->|no| Z - D -->|yes| E["RecommendationContextBuilder: identify prediction fields + context columns"] - E --> F{Does this entity have any prediction fields?} - F -->|no — add entity to skip cache| Z - F -->|yes| G["DB query: up to 2000 context rows (ORDER BY modifiedAt DESC)"] - G --> H["cds-feature-ai-core: resolveResourceGroup → resolveDeploymentId → inferenceClient"] - H --> I["RptInferenceClient.predict(predictRow, contextRows, columns) POST /v2/inference/deployments/{id}/predict"] - I --> J["RecommendationResultParser: type-convert + resolve @Common.Text descriptions"] - J --> K["Inject SAP_Recommendations into response row"] - K --> L["Return enriched response"] + classDef process fill:#fff,stroke:#000,color:#000 + classDef skip fill:#f4f4f4,stroke:#000,color:#000 + + START@{ shape: sm-circ, label: "Small start" } --> A(["OData GET — IsActiveEntity=false"]):::process + A --> B(["FioriRecommendationHandler @After(entity='*') afterRead(...)"]):::process + B --> C{In skip cache?} + C -->|yes| C1(["skip"]):::process + C1 --> L(["return response"]):::skip + C -->|no| D{Single row?} + D -->|no| L + D -->|yes| E(["identify prediction fields and context columns"]):::process + E --> F{Has prediction fields?} + F -->|no| F1(["add to skip cache"]):::process + F1 --> L + F -->|yes| G(["read context rows from DB"]):::process + G --> I(["predict recommendation values using RPT-1"]):::process + I --> J(["do type conversion and resolve @Common.Text descriptions"]):::process + J --> K(["add recommendation values to response row"]):::process + K --> L + L --> END1@{ shape: framed-circle, label: "Stop" } + + style START fill:#000,stroke:#000,color:#000 + style END1 fill:#fff,stroke:#000,stroke-width:3px,color:#000 + ``` -#### No-prediction-cache Invalidation +##### Context row selection -``` -ExtensibilityService - | - | EVENT_MODEL_CHANGED (tenantId) - v -RecommendationModelChangedHandler - | - | evict all entries in no-prediction-cache for tenantId - v -Next read re-evaluates +Context rows are fetched via `RecommendationContextBuilder.buildContextQuery()` directly against the `PersistenceService` (bypassing the application service layer — see [authorization note](#context-rows-and-instance-based-authorization) below). The query selects all non-draft, non-computed, non-readonly scalar columns of the same entity, filtered to rows where **all prediction columns are non-null** — rows that already have values for the fields being predicted. The current row is implicitly excluded because it still has null prediction values. Results are ordered by the most-recently-updated column (`@cds.on.update`) descending, or by key as fallback, and capped at `cds.ai.recommendations.contextRowLimit` (default 2000). + +This means selection is **recency-based, not similarity-based**: the model receives the most recently modified existing records as training context, not records that are semantically "near" the row being predicted. + +Context rows are **not cached** — every prediction fires a fresh database query. + +Context rows are currently fetched via `PersistenceService` (direct DB access), bypassing the application service and any instance-based authorization checks. This means a user could receive recommendations trained on rows they would not be allowed to read through the application service. +See: #128. + +#### MTX Model Change — Cache Invalidation + +```mermaid +%%{init: {'theme': 'base', 'themeVariables': {'edgeLabelBackground': '#ffffff00', 'fontSize': '14px', 'primaryColor': '#fff', 'primaryBorderColor': '#000', 'primaryTextColor': '#000'}}}%% +flowchart TD + classDef process fill:#fff,stroke:#000,color:#000 + + START@{ shape: sm-circ, label: "start" } --> A(["ExtensibilityService fires EVENT_MODEL_CHANGED (tenantId)"]):::process + A --> B(["RecommendationModelChangedHandler @On(EVENT_MODEL_CHANGED)"]):::process + B --> C(["evict all skip cache entries for tenantId"]):::process + C --> END1@{ shape: framed-circle, label: "stop" } + + style START fill:#000,stroke:#000,color:#000 + style END1 fill:#000,stroke:#000,stroke-width:3px,color:#000 ``` --- From d7ec81387668dbfe0c9748d4e48544f33e5da5b4 Mon Sep 17 00:00:00 2001 From: Lisa Julia Nebel Date: Thu, 6 Aug 2026 18:51:28 +0200 Subject: [PATCH 06/26] Add Architecture Decisions section in cds-feature-recommendations --- .../docs/architecture.md | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/cds-feature-recommendations/docs/architecture.md b/cds-feature-recommendations/docs/architecture.md index 8287bca..7827ec3 100644 --- a/cds-feature-recommendations/docs/architecture.md +++ b/cds-feature-recommendations/docs/architecture.md @@ -16,12 +16,13 @@ - [MTX Model Change — Cache Invalidation](#mtx-model-change--cache-invalidation) - [Tests](#tests) - [Quality Tools](#quality-tools) +- [Architecture Decisions](#architecture-decisions) --- ## Purpose -Automatically injects AI-powered field recommendations from the [SAP RPT-1](https://help.sap.com/docs/sap-ai-core/generative-ai/sap-rpt-1) tabular prediction foundation model into Fiori Elements OData responses for draft-enabled entities. Zero application code required. +Automatically injects AI-powered field recommendations from the [SAP-RPT-1](https://help.sap.com/docs/sap-ai-core/generative-ai/sap-rpt-1) tabular prediction foundation model into Fiori Elements OData responses for draft-enabled entities. Zero application code required. → [README](../README.md) @@ -159,3 +160,37 @@ End-to-end integration tests covering the full recommendation pipeline against a ## Quality Tools → [CI Checks and static analysis](../../CONTRIBUTING.md#ci-checks) + +--- + +## Architecture Decisions + +### Annotation-driven activation + +**Context:** Recommendations need to work across any CAP application that has value-list fields on draft-enabled entities, without requiring application developers to write handler code or configure anything beyond the CDS model annotations they already need for Fiori value help. + +**Decision:** Annotation-driven activation. `FioriRecommendationHandler` registers as an `@After(entity="*")` handler on all application services and derives prediction targets from the CDS model on each request. The `@cap-js/ai` Node.js CDS plugin adds the `SAP_Recommendations` navigation property to the model at build time so the predictions are serialized in the OData response without application changes. The trade-off is less flexibility — application code cannot currently override the inference call or observe the raw prediction result — tracked in [#110](https://github.com/cap-java/cds-ai/issues/110). + +--- + +### Recency-based context row selection + +**Context:** RPT-1 is a tabular prediction model that learns patterns from example rows (context rows) provided alongside the row to predict. The quality of predictions depends on the relevance of the context. Fetching all rows is impractical for large tables, and most models als impose a limit on the context rows (e.g. 2048 for SAP-RPT-1 https://help.sap.com/docs/sap-ai-core/generative-ai/sap-rpt-1#sap-rpt-models). + +**Solutions considered:** +- **Similarity-based selection** — select rows most semantically similar to the current row (e.g. by embedding distance or matching field values). This would select a better training context but requires additional infrastructure and adds much more complexity. +- **Recency-based selection (most recently modified first)** — orders by `@cds.on.update` descending, capped at `cds.ai.recommendations.contextRowLimit` (default 2000). Favors the most up-to-date data and requires no additional infrastructure. +**Decision:** Recency-based selection. The assumption is that recent records reflect the current state of the data better than older ones, making them more representative training context for the current user's editing patterns. Similarity-based selection remains a possible future improvement ([#128](https://github.com/cap-java/cds-ai/issues/128)) but was rejected for the initial version due to infrastructure requirements. + +--- + +### Miss-only entity skip cache + +**Context:** `RecommendationContextBuilder` derives prediction columns by scanning the CDS model on every request. For entities with no value-list fields, this scan is repeated on every OData GET — wasteful, since the model only changes on MTX upgrades. + +**Solutions considered:** +- **No cache** — simple but scans the model on every request for every entity, including those that will never have predictions. +- **Cache misses only (`Boolean` flag)** — entities with no prediction columns are cached; entities with columns are re-scanned every request. Small cache, but doesn't help for the common case of entities that do have predictions. +- **Cache the prediction column set (`Set`)** — cache the derived column names for all entities, using an empty set for no-prediction entities. Eliminates the per-request model scan entirely; slightly larger cache values. + +**Decision:** Cache misses only for now (`Cache`). The per-request model scan for entities that *do* have predictions was accepted as acceptable overhead in the initial version. Caching `Set` instead is tracked as a follow-up in [#129](https://github.com/cap-java/cds-ai/issues/129). The cache is keyed by `:` and invalidated on `EVENT_MODEL_CHANGED` so MTX model upgrades are reflected without a restart. From e8125868b08098def517d94cce9c96f61922268a Mon Sep 17 00:00:00 2001 From: Lisa Julia Nebel Date: Thu, 6 Aug 2026 19:00:25 +0200 Subject: [PATCH 07/26] Add Architecture Decisions section in cds-feature-ai-core plus further minor changes --- cds-feature-ai-core/docs/architecture.md | 123 ++++++++++++++++------- 1 file changed, 89 insertions(+), 34 deletions(-) diff --git a/cds-feature-ai-core/docs/architecture.md b/cds-feature-ai-core/docs/architecture.md index bf69c96..4d933bb 100644 --- a/cds-feature-ai-core/docs/architecture.md +++ b/cds-feature-ai-core/docs/architecture.md @@ -15,6 +15,7 @@ - [Inference Client Resolution](#inference-client-resolution) - [Tests](#tests) - [Quality Tools](#quality-tools) +- [Architecture Decisions](#architecture-decisions) --- @@ -34,6 +35,7 @@ Bridges CAP Java to SAP AI Core's management and inference REST APIs, providing | `com.github.ben-manes.caffeine:caffeine` | Thread-safe in-process caching for `tenantId → resourceGroupId` and `resourceGroupId::configName → deploymentId` mappings (1 h TTL, 10k max per cache). | | `io.github.resilience4j:resilience4j-retry` | Exponential backoff (initial 300 ms, doubling, max 30 s, up to 10 attempts) on 403/404/412 responses from AI Core - needed because resource group creation is asyncronous. | | `com.sap.cds:cds-services-api/-impl/-utils` | CAP Java integration — used to integrate the plugin into the CAP runtime. | +| CAP Java `DeploymentService` | MTX lifecycle hook: `AICoreSetupHandler` subscribes to `SubscribeEvent` (`@After LATE`) and `UnsubscribeEvent` (`@Before EARLY`) to create/delete per-tenant resource groups automatically. | --- @@ -66,7 +68,20 @@ The three custom events are **not declared in the CDS model** — they are Java- ### Public API -→ [Programmatic Usage in README](../README.md#programmatic-usage) +Three custom CDS events are the only stable contract. All other classes are internal. + +```java +// Resolve resource group for the current tenant +String rgId = aiCoreService.resourceGroup(); + +// Resolve (or lazily create) a deployment matching the given model spec +String deploymentId = aiCoreService.deploymentId(rgId, RptModelSpec.rpt1()); + +// Obtain a pre-configured ApiClient for inference +ApiClient client = aiCoreService.inferenceClient(rgId, deploymentId); +``` + +See also [Programmatic Usage in README](../README.md#programmatic-usage). --- @@ -88,31 +103,31 @@ The three custom events are **not declared in the CDS model** — they are Java- → [Multi-Tenancy in README](../README.md#multi-tenancy) +MT mode is detected automatically at startup: if `cds.multiTenancy.sidecar.url` is set or a `DeploymentService` bean is present in the CAP service catalog, MT mode is active. Resource groups are named `{resourceGroupPrefix}{tenantId}` (default prefix: `cds-`) and labelled `ext.ai.sap.com/CDS_TENANT_ID = ` so they can be looked up by tenant. + --- ### Key Flows #### Tenant Subscribe -``` -CAP MTX DeploymentService - | - | SubscribeEvent @After(LATE) - v -AICoreSetupHandler - | - | resolveResourceGroup(tenantId) - v -DeploymentResolver - | - | GET /v2/admin/resourceGroups?labelFilter=CDS_TENANT_ID=tenantId - v -SAP AI Core - | - | (if absent) POST /v2/admin/resourceGroups - v -resourceGroupId (cached 1h after last access — subsequent calls skip the AI Core management API; - if a resource group is deleted or reassigned externally, the plugin won't notice until the cache expires after 1h or the app restarts) +```mermaid +%%{init: {'theme': 'base', 'themeVariables': {'edgeLabelBackground': '#ffffff00', 'fontSize': '14px', 'primaryColor': '#fff', 'primaryBorderColor': '#000', 'primaryTextColor': '#000'}}}%% +flowchart TD + classDef process fill:#fff,stroke:#000,color:#000 + classDef skip fill:#f4f4f4,stroke:#000,color:#000 + + START@{ shape: sm-circ, label: "start" } --> A(["CAP MTX DeploymentService fires SubscribeEvent @After(LATE)"]):::process + A --> B(["AICoreSetupHandler: resolveResourceGroup(tenantId)"]):::process + B --> C(["GET /v2/admin/resourceGroups?labelFilter=CDS_TENANT_ID=tenantId"]):::process + C --> D{resource group found?} + D -->|yes| E(["cache resourceGroupId (expireAfterAccess 1h)"]):::process + D -->|no| F(["POST /v2/admin/resourceGroups"]):::process + F --> E + E --> END1@{ shape: framed-circle, label: "stop" } + + style START fill:#000,stroke:#000,color:#000 + style END1 fill:#000,stroke:#000,stroke-width:3px,color:#000 ``` #### Inference Client Resolution @@ -197,20 +212,18 @@ flowchart TD #### Tenant Unsubscribe -``` -CAP MTX DeploymentService - | - | UnsubscribeEvent @Before(EARLY) - v -AICoreSetupHandler - | - | DELETE /v2/admin/resourceGroups/{id} - v -SAP AI Core - | - | invalidateTenant(tenantId) — evicts tenantResourceGroupCache and deploymentCache (which was filled on first call to resolveDeployment) entries for this tenant - v -(done) +```mermaid +%%{init: {'theme': 'base', 'themeVariables': {'edgeLabelBackground': '#ffffff00', 'fontSize': '14px', 'primaryColor': '#fff', 'primaryBorderColor': '#000', 'primaryTextColor': '#000'}}}%% +flowchart TD + classDef process fill:#fff,stroke:#000,color:#000 + + START@{ shape: sm-circ, label: "start" } --> A(["CAP MTX DeploymentService fires UnsubscribeEvent @Before(EARLY)"]):::process + A --> B(["AICoreSetupHandler: DELETE /v2/admin/resourceGroups/{id}"]):::process + B --> C(["invalidateTenant(tenantId) — evicts tenantResourceGroupCache and deploymentCache entries"]):::process + C --> END1@{ shape: framed-circle, label: "stop" } + + style START fill:#000,stroke:#000,color:#000 + style END1 fill:#000,stroke:#000,stroke-width:3px,color:#000 ``` --- @@ -225,3 +238,45 @@ End-to-end integration tests against a real AI Core instance live in [`integrati ## Quality Tools → [CI Checks and static analysis](../../CONTRIBUTING.md#ci-checks) + +--- + +## Architecture Decisions + +### Wrapping the AI SDK in a CAP service for multi-tenant isolation + +**Context:** The SAP AI SDK (`com.sap.ai.sdk:ai-core`) provides API clients with no CAP integration. Plugins like `cds-feature-recommendations` need to resolve a resource group, a deployment, and an inference client on every request — but should not need to know about AI Core internals, tenant routing, or caching. + +**Key boundary condition:** The AI SDK's [`DestinationResolver`](https://github.com/SAP/ai-sdk-java/blob/main/core/src/main/java/com/sap/ai/sdk/core/DestinationResolver.java) always connects using `OnBehalfOf.TECHNICAL_USER_PROVIDER` — the provider tenant's service binding. There is no per-subscriber credential mechanism in the SDK. Tenant isolation is the caller's responsibility and is achieved entirely by setting the `AI-Resource-Group` HTTP header on each request to a resource group that belongs to the subscriber tenant. This is a hard constraint imposed by the SDK: any CAP integration on top of it must manage per-tenant resource group resolution itself — there is no way to "just pass a tenant ID" to the SDK and have it route correctly. +Beyond billing, proper per-tenant resource groups are also important for call history separation in AI Core: without them, all tenants' inference calls would appear under the same resource group in the AI Core audit log. + +**Decision:** Expose the three resolution steps (`resourceGroup`, `deploymentId`, `inferenceClient`) as custom CDS events on the `AICore` service. The `resourceGroup` event resolves the correct per-tenant resource group ID from the current CAP request context (`UserInfo.getTenant()`), which is then threaded through to `deploymentId` and `inferenceClient`. Every AI Core call made by `AICoreApiHandler` carries this resource group ID as the `AI-Resource-Group` header, satisfying the SDK's constraint. +Callers invoke the `AICoreService` Java API (`resourceGroup()`, `deploymentId()`, `inferenceClient()`), which emits the corresponding CDS events; `AICoreApiHandler` handles them. This keeps the AI SDK entirely internal and gives application code a standard `@On`/`@After` handler hook to override or observe each step if needed. + +--- + +### Caching resource group ids and deployment ids + +**Context:** Resolving an inference-ready `ApiClient` requires three sequential remote calls to AI Core (resource group lookup, deployment lookup/creation, client construction). AI Core deployments may not exist yet on first use and take minutes to reach RUNNING state. Calling the management API on every OData read would be prohibitively slow. + +**Solutions considered:** +- **`TenantAwareCache` (CAP built-in)** — `com.sap.cds.services.utils.TenantAwareCache` provides tenant-scoped invalidation natively and is already on the classpath via `cds-services-utils`. Not used yet but a candidate to replace the current manual Caffeine caches; see [#129](https://github.com/cap-java/cds-ai/issues/129). + - **In-process Caffeine cache per step** — zero additional infrastructure, thread-safe, configurable TTL. Accepted: the worst case (cache miss on restart or TTL expiry) is a single slow request; subsequent requests are fast. + +**Decision:** Cache `tenantId → resourceGroupId` and `resourceGroupId::configName → deploymentId` in two separate Caffeine caches with 1 h expire-after-access TTL. The deployment cache entry is validated on each cache hit (a `GET` to verify RUNNING/PENDING status) to detect externally stopped deployments. The resource group cache has no hit-validation — a stale entry causes failures until the TTL expires; this was accepted as an acceptable trade-off given that resource groups are rarely deleted externally. + +--- + +### Preventing duplicate AI deployments + +**Context:** Multiple requests may arrive concurrently before any deployment exists (e.g. on cold start of a new tenant). Without coordination, each request would independently discover the absence of a deployment and try to create one, resulting in duplicate deployments. + +**Decision:** Use a `ConcurrentHashMap` as a lock registry, synchronized on the value for the specific key being resolved. Only the first thread for a given key enters `findOrCreateDeployment`; subsequent threads wait and then find the deployment already in the cache. + +--- + +### Resilience4j exponential backoff for asynchronous AI Core operations + +**Context:** AI Core resource group and deployment creation is asynchronous. After a `POST /v2/admin/resourceGroups` or `POST /v2/lm/deployments`, subsequent calls may return 403 or 412 (precondition failed) until the resource is fully provisioned. Polling is necessary to wait for a deployment to reach RUNNING status. + +**Decision:** Use Resilience4j retry with exponential backoff (300 ms initial delay, doubling each attempt, capped at 30 s, max 10 attempts) on 403/404/412 responses from: `POST /v2/lm/deployments` (creation) and `GET /v2/lm/deployments/{id}` (polling until RUNNING). The same retry strategy covers both the "resource not yet available" and "deployment not yet running" cases. From 77f067f55b8bf0e68b2a40b2d09b932f04ee53c2 Mon Sep 17 00:00:00 2001 From: Lisa Julia Nebel Date: Wed, 12 Aug 2026 22:04:10 +0200 Subject: [PATCH 08/26] Update cds-feature-recommendations/docs/architecture.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Adrian Görler Signed-off-by: Lisa Julia Nebel --- cds-feature-recommendations/docs/architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cds-feature-recommendations/docs/architecture.md b/cds-feature-recommendations/docs/architecture.md index 7827ec3..26d9616 100644 --- a/cds-feature-recommendations/docs/architecture.md +++ b/cds-feature-recommendations/docs/architecture.md @@ -129,7 +129,7 @@ This means selection is **recency-based, not similarity-based**: the model recei Context rows are **not cached** — every prediction fires a fresh database query. Context rows are currently fetched via `PersistenceService` (direct DB access), bypassing the application service and any instance-based authorization checks. This means a user could receive recommendations trained on rows they would not be allowed to read through the application service. -See: #128. +See: [#128](https://github.com/cap-java/cds-ai/issues/128). #### MTX Model Change — Cache Invalidation From 335f8869dfbfd4e93050c55b6397fa1c3ce0f987 Mon Sep 17 00:00:00 2001 From: Lisa Julia Nebel Date: Wed, 12 Aug 2026 22:04:36 +0200 Subject: [PATCH 09/26] Update cds-feature-ai-core/docs/architecture.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Adrian Görler Signed-off-by: Lisa Julia Nebel --- cds-feature-ai-core/docs/architecture.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cds-feature-ai-core/docs/architecture.md b/cds-feature-ai-core/docs/architecture.md index 4d933bb..97a6899 100644 --- a/cds-feature-ai-core/docs/architecture.md +++ b/cds-feature-ai-core/docs/architecture.md @@ -21,7 +21,9 @@ ## Purpose -Bridges CAP Java to SAP AI Core's management and inference REST APIs, providing resource group management, deployment lifecycle, and inference client resolution as a CAP service. At the time of writing, `com.sap.ai.sdk:ai-core` offered no CAP integration — only raw REST API clients — so this plugin fills that gap. +CAP Java applications need to have to access to AI Core (`com.sap.ai.sdk:ai-core`) to manage resource groups and deployments, and to access an inference client. At the time of writing, AI Core offers no CAP integration — only raw REST API clients. + +This plugin (`‎cds-feature-ai-core`) fills this gap. It bridges CAP Java to SAP AI Core's management and inference REST APIs. It provides resource group management, deployment lifecycle, and inference client resolution as a CAP service. → [README](../README.md) From 889e56187390fcf8c1af2ba500b8176bdd78ae0b Mon Sep 17 00:00:00 2001 From: Lisa Julia Nebel Date: Wed, 12 Aug 2026 22:05:00 +0200 Subject: [PATCH 10/26] Update cds-feature-ai-core/docs/architecture.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Adrian Görler Signed-off-by: Lisa Julia Nebel --- cds-feature-ai-core/docs/architecture.md | 1 - 1 file changed, 1 deletion(-) diff --git a/cds-feature-ai-core/docs/architecture.md b/cds-feature-ai-core/docs/architecture.md index 97a6899..9f37699 100644 --- a/cds-feature-ai-core/docs/architecture.md +++ b/cds-feature-ai-core/docs/architecture.md @@ -147,7 +147,6 @@ The three steps map directly to the three `EventContext` interfaces and are exec Emitted by any caller (e.g. `cds-feature-recommendations`) via `AICoreService.resourceGroup()` to resolve the AI Core resource group ID for the current tenant. In single-tenant mode, the configured default resource group is returned directly. In multi-tenant mode, `DeploymentResolver` looks up the resource group by the `ext.ai.sap.com/CDS_TENANT_ID` label with a `GET` to `/v2/admin/resourceGroups`. The result is cached for 1 h (expire-after-access) so the AI Core management API is not called on every request. If no resource group is found, it creates one via `POST` to `/v2/admin/resourceGroups` (tolerating 409 conflicts), caches the result, and returns the resource group ID. Unlike the deployment cache, there is **no validation on cache hits** — if a resource group is deleted externally during that window, the stale ID stays cached and subsequent prediction requests will fail until the entry expires or the app restarts. - ##### Event 2: deploymentId — invoked with `resourceGroupId` Emitted by callers via `AICoreService.deploymentId(rgId, spec)` to resolve (or lazily create) a running deployment matching the given `ModelDeploymentSpec` inside the resource group. `DeploymentResolver` first checks its deployment cache; on a cache miss or invalid cached entry it queries AI Core for an existing RUNNING/PENDING deployment, and — if none exists — creates the configuration and deployment, then polls until RUNNING with exponential backoff: Resilience4j exponential backoff (300 ms initial, doubling, capped at 30 s, max 10 attempts) on: 403/412 during deployment creation (`POST /v2/lm/deployments`); 403/404/412 during deployment polling (`GET /v2/lm/deployments`). A `ConcurrentHashMap` per-key lock prevents duplicate deployments from being created under concurrent first-use requests. The resolved deployment ID is cached for 1 h. From 40852e36bb641f60ec4c4f7c375e66be737affea Mon Sep 17 00:00:00 2001 From: Lisa Julia Nebel Date: Wed, 12 Aug 2026 22:05:11 +0200 Subject: [PATCH 11/26] Update cds-feature-ai-core/docs/architecture.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Adrian Görler Signed-off-by: Lisa Julia Nebel --- cds-feature-ai-core/docs/architecture.md | 1 - 1 file changed, 1 deletion(-) diff --git a/cds-feature-ai-core/docs/architecture.md b/cds-feature-ai-core/docs/architecture.md index 9f37699..91f5864 100644 --- a/cds-feature-ai-core/docs/architecture.md +++ b/cds-feature-ai-core/docs/architecture.md @@ -151,7 +151,6 @@ Unlike the deployment cache, there is **no validation on cache hits** — if a r Emitted by callers via `AICoreService.deploymentId(rgId, spec)` to resolve (or lazily create) a running deployment matching the given `ModelDeploymentSpec` inside the resource group. `DeploymentResolver` first checks its deployment cache; on a cache miss or invalid cached entry it queries AI Core for an existing RUNNING/PENDING deployment, and — if none exists — creates the configuration and deployment, then polls until RUNNING with exponential backoff: Resilience4j exponential backoff (300 ms initial, doubling, capped at 30 s, max 10 attempts) on: 403/412 during deployment creation (`POST /v2/lm/deployments`); 403/404/412 during deployment polling (`GET /v2/lm/deployments`). A `ConcurrentHashMap` per-key lock prevents duplicate deployments from being created under concurrent first-use requests. The resolved deployment ID is cached for 1 h. - ##### Event 3: inferenceClient — invoked with `resourceGroupId` and `deploymentId` Emitted by callers via `AICoreService.inferenceClient(rgId, deploymentId)` to obtain a pre-configured `ApiClient` ready to make prediction requests against a specific deployment. The handler delegates to the SAP AI SDK's `AiCoreService` to build an inference destination scoped to the resource group and deployment, then wraps it in an `ApiClient`. No caching — the client is lightweight to construct and callers are expected to obtain it once per request. From 20d731d5190d658c5f89c0623603fe50c30a1776 Mon Sep 17 00:00:00 2001 From: Lisa Julia Nebel Date: Wed, 12 Aug 2026 22:05:26 +0200 Subject: [PATCH 12/26] Update cds-feature-ai-core/docs/architecture.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Adrian Görler Signed-off-by: Lisa Julia Nebel --- cds-feature-ai-core/docs/architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cds-feature-ai-core/docs/architecture.md b/cds-feature-ai-core/docs/architecture.md index 91f5864..48d44e4 100644 --- a/cds-feature-ai-core/docs/architecture.md +++ b/cds-feature-ai-core/docs/architecture.md @@ -149,7 +149,7 @@ Unlike the deployment cache, there is **no validation on cache hits** — if a r ##### Event 2: deploymentId — invoked with `resourceGroupId` -Emitted by callers via `AICoreService.deploymentId(rgId, spec)` to resolve (or lazily create) a running deployment matching the given `ModelDeploymentSpec` inside the resource group. `DeploymentResolver` first checks its deployment cache; on a cache miss or invalid cached entry it queries AI Core for an existing RUNNING/PENDING deployment, and — if none exists — creates the configuration and deployment, then polls until RUNNING with exponential backoff: Resilience4j exponential backoff (300 ms initial, doubling, capped at 30 s, max 10 attempts) on: 403/412 during deployment creation (`POST /v2/lm/deployments`); 403/404/412 during deployment polling (`GET /v2/lm/deployments`). A `ConcurrentHashMap` per-key lock prevents duplicate deployments from being created under concurrent first-use requests. The resolved deployment ID is cached for 1 h. +Emitted by callers via `AICoreService.deploymentId(rgId, spec)` to resolve (or lazily create) a running deployment matching the given `ModelDeploymentSpec` inside the resource group. `DeploymentResolver` first checks its deployment cache. On a cache miss or invalid cached entry it queries AI Core for an existing RUNNING/PENDING deployment, and — if none exists — creates the configuration and deployment. It then polls until RUNNING with exponential backoff: Resilience4j exponential backoff (300 ms initial, doubling, capped at 30 s, max 10 attempts) on: 403/412 during deployment creation (`POST /v2/lm/deployments`); 403/404/412 during deployment polling (`GET /v2/lm/deployments`). A `ConcurrentHashMap` per-key lock prevents duplicate deployments from being created under concurrent first-use requests. The resolved deployment ID is cached for 1 h. ##### Event 3: inferenceClient — invoked with `resourceGroupId` and `deploymentId` From 769b1ada7ae4ff5c45270e3b92f7a83181a8180c Mon Sep 17 00:00:00 2001 From: Lisa Julia Nebel Date: Wed, 12 Aug 2026 22:05:53 +0200 Subject: [PATCH 13/26] Update cds-feature-ai-core/docs/architecture.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Adrian Görler Signed-off-by: Lisa Julia Nebel --- cds-feature-ai-core/docs/architecture.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/cds-feature-ai-core/docs/architecture.md b/cds-feature-ai-core/docs/architecture.md index 48d44e4..e0551e4 100644 --- a/cds-feature-ai-core/docs/architecture.md +++ b/cds-feature-ai-core/docs/architecture.md @@ -31,13 +31,13 @@ This plugin (`‎cds-feature-ai-core`) fills this gap. It bridges CAP Java to SA ## Dependencies -| Dependency | Why | -|---|---| -| `com.sap.ai.sdk:ai-core` (SAP AI SDK) | Provides the generated `DeploymentApi`, `ConfigurationApi`, `ResourceGroupApi`, and `ApiClient` types used to call the AI Core REST API. The plugin wraps these behind CDS events so callers never deal with the AI SDK directly. | -| `com.github.ben-manes.caffeine:caffeine` | Thread-safe in-process caching for `tenantId → resourceGroupId` and `resourceGroupId::configName → deploymentId` mappings (1 h TTL, 10k max per cache). | -| `io.github.resilience4j:resilience4j-retry` | Exponential backoff (initial 300 ms, doubling, max 30 s, up to 10 attempts) on 403/404/412 responses from AI Core - needed because resource group creation is asyncronous. | -| `com.sap.cds:cds-services-api/-impl/-utils` | CAP Java integration — used to integrate the plugin into the CAP runtime. | -| CAP Java `DeploymentService` | MTX lifecycle hook: `AICoreSetupHandler` subscribes to `SubscribeEvent` (`@After LATE`) and `UnsubscribeEvent` (`@Before EARLY`) to create/delete per-tenant resource groups automatically. | +| Dependency | Maven | Why | +|---|---|---| +| SAP AI SDK | `com.sap.ai.sdk:ai-core`| Provides the generated `DeploymentApi`, `ConfigurationApi`, `ResourceGroupApi`, and `ApiClient` types used to call the AI Core REST API. The plugin wraps these behind CDS events so callers never deal with the AI SDK directly. | +| Caffeine | `com.github.ben-manes.caffeine:caffeine` | Thread-safe in-process caching for `tenantId → resourceGroupId` and `resourceGroupId::configName → deploymentId` mappings (1 h TTL, 10k max per cache). | +| Resilience4j | `io.github.resilience4j:resilience4j-retry` | Exponential backoff (initial 300 ms, doubling, max 30 s, up to 10 attempts) on 403/404/412 responses from AI Core - needed because resource group creation is asyncronous. | +| CAP Java | `com.sap.cds:cds-services-api/-impl/-utils` | CAP Java integration — used to integrate the plugin into the CAP runtime. | +| CAP Java `DeploymentService` | | MTX lifecycle hook: `AICoreSetupHandler` subscribes to `SubscribeEvent` (`@After LATE`) and `UnsubscribeEvent` (`@Before EARLY`) to create/delete per-tenant resource groups automatically. | --- From d83ddd1cc7037052816a5d2f1a479f4980e645e4 Mon Sep 17 00:00:00 2001 From: Lisa Julia Nebel Date: Wed, 12 Aug 2026 22:06:06 +0200 Subject: [PATCH 14/26] Update cds-feature-recommendations/docs/architecture.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Adrian Görler Signed-off-by: Lisa Julia Nebel --- cds-feature-recommendations/docs/architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cds-feature-recommendations/docs/architecture.md b/cds-feature-recommendations/docs/architecture.md index 26d9616..8d27d02 100644 --- a/cds-feature-recommendations/docs/architecture.md +++ b/cds-feature-recommendations/docs/architecture.md @@ -22,7 +22,7 @@ ## Purpose -Automatically injects AI-powered field recommendations from the [SAP-RPT-1](https://help.sap.com/docs/sap-ai-core/generative-ai/sap-rpt-1) tabular prediction foundation model into Fiori Elements OData responses for draft-enabled entities. Zero application code required. +This plugin (`cds-feature-recommendations`) automatically injects AI-powered field recommendations from the [SAP-RPT-1](https://help.sap.com/docs/sap-ai-core/generative-ai/sap-rpt-1) tabular prediction foundation model into Fiori Elements OData responses for draft-enabled entities. Zero application code required. → [README](../README.md) From 63ecceceac4dc8109e3f8d8410454a962bec2817 Mon Sep 17 00:00:00 2001 From: Lisa Julia Nebel Date: Wed, 12 Aug 2026 22:06:17 +0200 Subject: [PATCH 15/26] Update cds-feature-recommendations/docs/architecture.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Adrian Görler Signed-off-by: Lisa Julia Nebel --- cds-feature-recommendations/docs/architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cds-feature-recommendations/docs/architecture.md b/cds-feature-recommendations/docs/architecture.md index 8d27d02..8eb0271 100644 --- a/cds-feature-recommendations/docs/architecture.md +++ b/cds-feature-recommendations/docs/architecture.md @@ -83,7 +83,7 @@ Cache<":", Boolean> 10k max, no TTL → invalidated by RecommendationModelChangedHandler on model change ``` -The cache is keyed by `:` and is invalidated on `ExtensibilityService.EVENT_MODEL_CHANGED` — ensuring that model upgrades (which may add or remove value-list annotations) are reflected without a restart, see also [MTX Model Change — Cache Invalidation](#mtx-model-change--cache-invalidation). This cache might be replaced with #129. +The cache is keyed by `:` and is invalidated on `ExtensibilityService.EVENT_MODEL_CHANGED` — ensuring that model upgrades (which may add or remove value-list annotations) are reflected without a restart, see also [MTX Model Change — Cache Invalidation](#mtx-model-change--cache-invalidation). This cache might be replaced with [#129](https://github.com/cap-java/cds-ai/issues/129). Currently the cache stores only **misses** — entities that have no prediction columns. For entities that *do* have prediction columns, `RecommendationContextBuilder` re-derives them from the CDS model on every request. An alternative design would cache `Set` (the prediction column names) instead of `Boolean`, using an empty set for the no-prediction case. This would eliminate the per-request model scan for all entities, at the cost of a slightly larger cache value. From 1b4c9186c27d82e92952674f8c3c996527c51f4a Mon Sep 17 00:00:00 2001 From: Lisa Julia Nebel Date: Wed, 12 Aug 2026 22:06:30 +0200 Subject: [PATCH 16/26] Update cds-feature-ai-core/docs/architecture.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Adrian Görler Signed-off-by: Lisa Julia Nebel --- cds-feature-ai-core/docs/architecture.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cds-feature-ai-core/docs/architecture.md b/cds-feature-ai-core/docs/architecture.md index e0551e4..06dd1bd 100644 --- a/cds-feature-ai-core/docs/architecture.md +++ b/cds-feature-ai-core/docs/architecture.md @@ -134,7 +134,8 @@ flowchart TD #### Inference Client Resolution -Every prediction request or inference call requires a fully-configured `ApiClient` that is scoped to a specific AI Core deployment. The challenge is that creating this client requires three sequential steps +Every prediction request or inference call requires a fully-configured `ApiClient` that is scoped to a specific AI Core deployment. The challenge is that creating this client requires three sequential steps: + — tenant → resource group - resource group + model spec (e.g. RPT-1) → deployment - deployment → ApiClient From 5abe764c6e5a3051be6d93f9f840955673df39ae Mon Sep 17 00:00:00 2001 From: Lisa Julia Nebel Date: Wed, 12 Aug 2026 22:06:50 +0200 Subject: [PATCH 17/26] Update cds-feature-ai-core/docs/architecture.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Adrian Görler Signed-off-by: Lisa Julia Nebel --- cds-feature-ai-core/docs/architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cds-feature-ai-core/docs/architecture.md b/cds-feature-ai-core/docs/architecture.md index 06dd1bd..548feaa 100644 --- a/cds-feature-ai-core/docs/architecture.md +++ b/cds-feature-ai-core/docs/architecture.md @@ -256,7 +256,7 @@ Callers invoke the `AICoreService` Java API (`resourceGroup()`, `deploymentId()` --- -### Caching resource group ids and deployment ids +### Caching resource group IDs and deployment IDs **Context:** Resolving an inference-ready `ApiClient` requires three sequential remote calls to AI Core (resource group lookup, deployment lookup/creation, client construction). AI Core deployments may not exist yet on first use and take minutes to reach RUNNING state. Calling the management API on every OData read would be prohibitively slow. From 5ae7e59ba51ac07c12f78006f2c27f543a1df84a Mon Sep 17 00:00:00 2001 From: Lisa Julia Nebel Date: Wed, 12 Aug 2026 22:07:07 +0200 Subject: [PATCH 18/26] Update cds-feature-ai-core/docs/architecture.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Adrian Görler Signed-off-by: Lisa Julia Nebel --- cds-feature-ai-core/docs/architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cds-feature-ai-core/docs/architecture.md b/cds-feature-ai-core/docs/architecture.md index 548feaa..4d822a8 100644 --- a/cds-feature-ai-core/docs/architecture.md +++ b/cds-feature-ai-core/docs/architecture.md @@ -261,7 +261,7 @@ Callers invoke the `AICoreService` Java API (`resourceGroup()`, `deploymentId()` **Context:** Resolving an inference-ready `ApiClient` requires three sequential remote calls to AI Core (resource group lookup, deployment lookup/creation, client construction). AI Core deployments may not exist yet on first use and take minutes to reach RUNNING state. Calling the management API on every OData read would be prohibitively slow. **Solutions considered:** -- **`TenantAwareCache` (CAP built-in)** — `com.sap.cds.services.utils.TenantAwareCache` provides tenant-scoped invalidation natively and is already on the classpath via `cds-services-utils`. Not used yet but a candidate to replace the current manual Caffeine caches; see [#129](https://github.com/cap-java/cds-ai/issues/129). +- **`TenantAwareCache` (intrenal API of CAP Java)** — `com.sap.cds.services.utils.TenantAwareCache` provides tenant-scoped invalidation natively and is already on the classpath via `cds-services-utils`. Not used yet but a candidate to replace the current manual Caffeine caches; see [#129](https://github.com/cap-java/cds-ai/issues/129). - **In-process Caffeine cache per step** — zero additional infrastructure, thread-safe, configurable TTL. Accepted: the worst case (cache miss on restart or TTL expiry) is a single slow request; subsequent requests are fast. **Decision:** Cache `tenantId → resourceGroupId` and `resourceGroupId::configName → deploymentId` in two separate Caffeine caches with 1 h expire-after-access TTL. The deployment cache entry is validated on each cache hit (a `GET` to verify RUNNING/PENDING status) to detect externally stopped deployments. The resource group cache has no hit-validation — a stale entry causes failures until the TTL expires; this was accepted as an acceptable trade-off given that resource groups are rarely deleted externally. From e77296301c4ab9b03ec446458ab4c2a3f9ef4abc Mon Sep 17 00:00:00 2001 From: Lisa Julia Nebel Date: Wed, 12 Aug 2026 22:07:37 +0200 Subject: [PATCH 19/26] Update cds-feature-recommendations/docs/architecture.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Adrian Görler Signed-off-by: Lisa Julia Nebel --- cds-feature-recommendations/docs/architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cds-feature-recommendations/docs/architecture.md b/cds-feature-recommendations/docs/architecture.md index 8eb0271..0b593c3 100644 --- a/cds-feature-recommendations/docs/architecture.md +++ b/cds-feature-recommendations/docs/architecture.md @@ -35,7 +35,7 @@ This plugin (`cds-feature-recommendations`) automatically injects AI-powered fie | [`cds-feature-ai-core`](../../cds-feature-ai-core/README.md) | Provides the `AICore` CDS service and `AICoreService` API used to resolve the resource group, deployment ID, and inference `ApiClient` for the RPT-1 model. Recommendations cannot function without an active AI Core connection. | | `@cap-js/ai` (Node.js CDS plugin) | At CDS build time, the plugin adds the `SAP_Recommendations` navigation property to draft-enabled entities that have value-list fields. Without this (or a manual CDS extension), predictions are computed but not serialized in OData responses. | | `com.sap.ai.sdk.foundationmodels:sap-rpt` (SAP AI SDK) | Provides the RPT-1 model client used to call the `/predict` endpoint. | -| `com.github.ben-manes.caffeine:caffeine` | Thread-safe in-process caching for the per-tenant entity skip cache (10k max, no TTL). This might be replaced in #129.| +| `com.github.ben-manes.caffeine:caffeine` | Thread-safe in-process caching for the per-tenant entity skip cache (10k max, no TTL). This might be replaced in [#129](https://github.com/cap-java/cds-ai/issues/129).| | `com.sap.cds:cds-services-api/-impl/-utils` | CAP Java integration — used to integrate the plugin into the CAP runtime. | --- From 977bb7c2734c034aba9c6b44d2760d4467d5eca4 Mon Sep 17 00:00:00 2001 From: Lisa Julia Nebel Date: Wed, 12 Aug 2026 22:11:49 +0200 Subject: [PATCH 20/26] Another comment from Adrian --- cds-feature-recommendations/docs/architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cds-feature-recommendations/docs/architecture.md b/cds-feature-recommendations/docs/architecture.md index 0b593c3..af1035d 100644 --- a/cds-feature-recommendations/docs/architecture.md +++ b/cds-feature-recommendations/docs/architecture.md @@ -167,7 +167,7 @@ End-to-end integration tests covering the full recommendation pipeline against a ### Annotation-driven activation -**Context:** Recommendations need to work across any CAP application that has value-list fields on draft-enabled entities, without requiring application developers to write handler code or configure anything beyond the CDS model annotations they already need for Fiori value help. +**Context:** Recommendations need to work across any CAP application that has value-list fields on draft-enabled entities, without requiring application developers to write handler code or configure anything beyond the CDS model annotations that are already required for Fiori value help. **Decision:** Annotation-driven activation. `FioriRecommendationHandler` registers as an `@After(entity="*")` handler on all application services and derives prediction targets from the CDS model on each request. The `@cap-js/ai` Node.js CDS plugin adds the `SAP_Recommendations` navigation property to the model at build time so the predictions are serialized in the OData response without application changes. The trade-off is less flexibility — application code cannot currently override the inference call or observe the raw prediction result — tracked in [#110](https://github.com/cap-java/cds-ai/issues/110). From 7d2b11feafa07dd527511c882feb0a6f8c8b64e6 Mon Sep 17 00:00:00 2001 From: Lisa Julia Nebel Date: Wed, 12 Aug 2026 22:17:50 +0200 Subject: [PATCH 21/26] Comment from Adrian about lock service that uses a db for preventing duplicate deployments with AI Core --- cds-feature-ai-core/docs/architecture.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cds-feature-ai-core/docs/architecture.md b/cds-feature-ai-core/docs/architecture.md index 4d822a8..27c3e70 100644 --- a/cds-feature-ai-core/docs/architecture.md +++ b/cds-feature-ai-core/docs/architecture.md @@ -274,6 +274,8 @@ Callers invoke the `AICoreService` Java API (`resourceGroup()`, `deploymentId()` **Decision:** Use a `ConcurrentHashMap` as a lock registry, synchronized on the value for the specific key being resolved. Only the first thread for a given key enters `findOrCreateDeployment`; subsequent threads wait and then find the deployment already in the cache. +**Limitation:** This per-key locking is in-process only — it does not coordinate across multiple application instances. Multiple applications starting up concurrently may each independently discover the absence of a deployment and create one, resulting in duplicate deployments. For preventing this, a lock service that uses a database would be needed, which CAP does not have, unfortunately, see also https://github.tools.sap/cap/dev/issues/156. + --- ### Resilience4j exponential backoff for asynchronous AI Core operations From a9eae656b00d640561a93ba4a79014e0f40c5966 Mon Sep 17 00:00:00 2001 From: Lisa Julia Nebel Date: Wed, 12 Aug 2026 22:25:18 +0200 Subject: [PATCH 22/26] Adjust Public API section according to Adrians comments --- cds-feature-ai-core/docs/architecture.md | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/cds-feature-ai-core/docs/architecture.md b/cds-feature-ai-core/docs/architecture.md index 27c3e70..63b94ae 100644 --- a/cds-feature-ai-core/docs/architecture.md +++ b/cds-feature-ai-core/docs/architecture.md @@ -70,17 +70,29 @@ The three custom events are **not declared in the CDS model** — they are Java- ### Public API -Three custom CDS events are the only stable contract. All other classes are internal. +Three custom CDS events are the only stable contract. All other classes are internal. Callers obtain the `AICore` service as a `RemoteService` (injected by CAP) and drive the three resolution steps by creating an `EventContext`, setting its inputs, emitting it, and reading back the result (see [`RecommendationConfiguration.java`](../../cds-feature-recommendations/src/main/java/com/sap/cds/feature/recommendation/RecommendationConfiguration.java) for a real-world example): ```java +RemoteService aiCoreService = ...; // injected as a CAP RemoteService + // Resolve resource group for the current tenant -String rgId = aiCoreService.resourceGroup(); +ResourceGroupContext rgCtx = ResourceGroupContext.create(); +aiCoreService.emit(rgCtx); +String resourceGroup = rgCtx.getResult(); // Resolve (or lazily create) a deployment matching the given model spec -String deploymentId = aiCoreService.deploymentId(rgId, RptModelSpec.rpt1()); +DeploymentIdContext depCtx = DeploymentIdContext.create(); +depCtx.setResourceGroupId(resourceGroup); +depCtx.setSpec(RptModelSpec.rpt1()); +aiCoreService.emit(depCtx); +String deploymentId = depCtx.getResult(); // Obtain a pre-configured ApiClient for inference -ApiClient client = aiCoreService.inferenceClient(rgId, deploymentId); +InferenceClientContext infCtx = InferenceClientContext.create(); +infCtx.setResourceGroupId(resourceGroup); +infCtx.setDeploymentId(deploymentId); +aiCoreService.emit(infCtx); +ApiClient client = infCtx.getResult(); ``` See also [Programmatic Usage in README](../README.md#programmatic-usage). From cd17c9a44d2813e47acd8df32331cb5dfebdff15 Mon Sep 17 00:00:00 2001 From: Lisa Julia Nebel Date: Thu, 13 Aug 2026 10:04:04 +0200 Subject: [PATCH 23/26] Update cds-feature-ai-core/docs/architecture.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Adrian Görler Signed-off-by: Lisa Julia Nebel --- cds-feature-ai-core/docs/architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cds-feature-ai-core/docs/architecture.md b/cds-feature-ai-core/docs/architecture.md index 63b94ae..36d3d8e 100644 --- a/cds-feature-ai-core/docs/architecture.md +++ b/cds-feature-ai-core/docs/architecture.md @@ -286,7 +286,7 @@ Callers invoke the `AICoreService` Java API (`resourceGroup()`, `deploymentId()` **Decision:** Use a `ConcurrentHashMap` as a lock registry, synchronized on the value for the specific key being resolved. Only the first thread for a given key enters `findOrCreateDeployment`; subsequent threads wait and then find the deployment already in the cache. -**Limitation:** This per-key locking is in-process only — it does not coordinate across multiple application instances. Multiple applications starting up concurrently may each independently discover the absence of a deployment and create one, resulting in duplicate deployments. For preventing this, a lock service that uses a database would be needed, which CAP does not have, unfortunately, see also https://github.tools.sap/cap/dev/issues/156. +**Limitation:** This per-key locking is in-process only — it does not coordinate across multiple application instances. Multiple applications starting up concurrently may each independently discover the absence of a deployment and create one, resulting in duplicate deployments. For preventing this, a lock service that uses a database would be needed, which CAP does not have, unfortunately. --- From e725d48b13a5b6a5b41eafdb147402933d970aa4 Mon Sep 17 00:00:00 2001 From: Lisa Julia Nebel Date: Thu, 13 Aug 2026 10:04:15 +0200 Subject: [PATCH 24/26] Update cds-feature-ai-core/docs/architecture.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Adrian Görler Signed-off-by: Lisa Julia Nebel --- cds-feature-ai-core/docs/architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cds-feature-ai-core/docs/architecture.md b/cds-feature-ai-core/docs/architecture.md index 36d3d8e..56a46e2 100644 --- a/cds-feature-ai-core/docs/architecture.md +++ b/cds-feature-ai-core/docs/architecture.md @@ -273,7 +273,7 @@ Callers invoke the `AICoreService` Java API (`resourceGroup()`, `deploymentId()` **Context:** Resolving an inference-ready `ApiClient` requires three sequential remote calls to AI Core (resource group lookup, deployment lookup/creation, client construction). AI Core deployments may not exist yet on first use and take minutes to reach RUNNING state. Calling the management API on every OData read would be prohibitively slow. **Solutions considered:** -- **`TenantAwareCache` (intrenal API of CAP Java)** — `com.sap.cds.services.utils.TenantAwareCache` provides tenant-scoped invalidation natively and is already on the classpath via `cds-services-utils`. Not used yet but a candidate to replace the current manual Caffeine caches; see [#129](https://github.com/cap-java/cds-ai/issues/129). +- **`TenantAwareCache` (internal API of CAP Java)** — `com.sap.cds.services.utils.TenantAwareCache` provides tenant-scoped invalidation natively and is already on the classpath via `cds-services-utils`. Not used yet but a candidate to replace the current manual Caffeine caches; see [#129](https://github.com/cap-java/cds-ai/issues/129). - **In-process Caffeine cache per step** — zero additional infrastructure, thread-safe, configurable TTL. Accepted: the worst case (cache miss on restart or TTL expiry) is a single slow request; subsequent requests are fast. **Decision:** Cache `tenantId → resourceGroupId` and `resourceGroupId::configName → deploymentId` in two separate Caffeine caches with 1 h expire-after-access TTL. The deployment cache entry is validated on each cache hit (a `GET` to verify RUNNING/PENDING status) to detect externally stopped deployments. The resource group cache has no hit-validation — a stale entry causes failures until the TTL expires; this was accepted as an acceptable trade-off given that resource groups are rarely deleted externally. From 9831284bcbdbe060edd7448d2f73b7f03c856b56 Mon Sep 17 00:00:00 2001 From: Lisa Julia Nebel Date: Thu, 13 Aug 2026 10:04:27 +0200 Subject: [PATCH 25/26] Update cds-feature-ai-core/docs/architecture.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Adrian Görler Signed-off-by: Lisa Julia Nebel --- cds-feature-ai-core/docs/architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cds-feature-ai-core/docs/architecture.md b/cds-feature-ai-core/docs/architecture.md index 56a46e2..b1e0c03 100644 --- a/cds-feature-ai-core/docs/architecture.md +++ b/cds-feature-ai-core/docs/architecture.md @@ -21,7 +21,7 @@ ## Purpose -CAP Java applications need to have to access to AI Core (`com.sap.ai.sdk:ai-core`) to manage resource groups and deployments, and to access an inference client. At the time of writing, AI Core offers no CAP integration — only raw REST API clients. +CAP Java applications need to access AI Core (`com.sap.ai.sdk:ai-core`) to manage resource groups and deployments, and to access an inference client. At the time of writing, AI Core offers no CAP integration — only raw REST API clients. This plugin (`‎cds-feature-ai-core`) fills this gap. It bridges CAP Java to SAP AI Core's management and inference REST APIs. It provides resource group management, deployment lifecycle, and inference client resolution as a CAP service. From 6f0acaad63f734e276091a9a3bf37e0dbc8fec66 Mon Sep 17 00:00:00 2001 From: Lisa Julia Nebel Date: Thu, 13 Aug 2026 10:04:41 +0200 Subject: [PATCH 26/26] Update cds-feature-ai-core/docs/architecture.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Adrian Görler Signed-off-by: Lisa Julia Nebel --- cds-feature-ai-core/docs/architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cds-feature-ai-core/docs/architecture.md b/cds-feature-ai-core/docs/architecture.md index b1e0c03..2f8beb0 100644 --- a/cds-feature-ai-core/docs/architecture.md +++ b/cds-feature-ai-core/docs/architecture.md @@ -35,7 +35,7 @@ This plugin (`‎cds-feature-ai-core`) fills this gap. It bridges CAP Java to SA |---|---|---| | SAP AI SDK | `com.sap.ai.sdk:ai-core`| Provides the generated `DeploymentApi`, `ConfigurationApi`, `ResourceGroupApi`, and `ApiClient` types used to call the AI Core REST API. The plugin wraps these behind CDS events so callers never deal with the AI SDK directly. | | Caffeine | `com.github.ben-manes.caffeine:caffeine` | Thread-safe in-process caching for `tenantId → resourceGroupId` and `resourceGroupId::configName → deploymentId` mappings (1 h TTL, 10k max per cache). | -| Resilience4j | `io.github.resilience4j:resilience4j-retry` | Exponential backoff (initial 300 ms, doubling, max 30 s, up to 10 attempts) on 403/404/412 responses from AI Core - needed because resource group creation is asyncronous. | +| Resilience4j | `io.github.resilience4j:resilience4j-retry` | Exponential backoff (initial 300 ms, doubling, max 30 s, up to 10 attempts) on 403/404/412 responses from AI Core - needed because resource group creation is asynchronous. | | CAP Java | `com.sap.cds:cds-services-api/-impl/-utils` | CAP Java integration — used to integrate the plugin into the CAP runtime. | | CAP Java `DeploymentService` | | MTX lifecycle hook: `AICoreSetupHandler` subscribes to `SubscribeEvent` (`@After LATE`) and `UnsubscribeEvent` (`@Before EARLY`) to create/delete per-tenant resource groups automatically. |