@viveka10/cap-ai-token-optimizer 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +83 -0
- package/LICENSE +21 -0
- package/README.md +753 -2
- package/SECURITY.md +39 -0
- package/cds-plugin.js +50 -0
- package/db/token-usage-log.cds +44 -0
- package/docs/existing-cap-app-on-btp.md +425 -0
- package/docs/integration-guide.md +140 -0
- package/index.d.ts +291 -0
- package/index.js +36 -0
- package/lib/cache/index.js +67 -0
- package/lib/client/adapters.js +263 -0
- package/lib/client/instrument.js +124 -0
- package/lib/config.js +128 -0
- package/lib/exporters/console.js +18 -0
- package/lib/exporters/db.js +169 -0
- package/lib/exporters/http.js +143 -0
- package/lib/exporters/index.js +42 -0
- package/lib/exporters/otel.js +126 -0
- package/lib/metrics/classifier.js +175 -0
- package/lib/metrics/collector.js +206 -0
- package/lib/metrics/events.js +124 -0
- package/lib/optimizer.js +467 -0
- package/lib/pipeline/index.js +90 -0
- package/lib/pipeline/messages.js +62 -0
- package/lib/pipeline/steps/dedupe.js +103 -0
- package/lib/pipeline/steps/history.js +63 -0
- package/lib/pipeline/steps/normalize.js +88 -0
- package/lib/pipeline/steps/outputCap.js +28 -0
- package/lib/pipeline/steps/tools.js +157 -0
- package/lib/pipeline/text.js +166 -0
- package/lib/tokens/estimator.js +163 -0
- package/lib/util/cds.js +13 -0
- package/lib/util/hash.js +40 -0
- package/lib/util/log.js +37 -0
- package/lib/util/peer.js +55 -0
- package/package.json +141 -4
- package/srv/admin-service.cds +83 -0
- package/srv/admin-service.js +41 -0
- package/srv/admin-usage-log.cds +8 -0
package/SECURITY.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported versions
|
|
4
|
+
|
|
5
|
+
Security fixes are made for the latest minor release. Before 1.0.0, that is the latest
|
|
6
|
+
0.x release.
|
|
7
|
+
|
|
8
|
+
| Version | Supported |
|
|
9
|
+
|---|---|
|
|
10
|
+
| latest 0.x | ✅ |
|
|
11
|
+
| older | ❌ |
|
|
12
|
+
|
|
13
|
+
## Reporting a vulnerability
|
|
14
|
+
|
|
15
|
+
Please do **not** open a public issue for security problems.
|
|
16
|
+
|
|
17
|
+
Report them privately through GitHub:
|
|
18
|
+
[Security → Report a vulnerability](https://github.com/viveka10/npm_token_optimizer/security/advisories/new).
|
|
19
|
+
Include the package version, a description, and steps to reproduce if possible.
|
|
20
|
+
|
|
21
|
+
You can expect an acknowledgement within 7 days. Fixes are released as patch versions and
|
|
22
|
+
noted in the [CHANGELOG](CHANGELOG.md).
|
|
23
|
+
|
|
24
|
+
## How this package handles data
|
|
25
|
+
|
|
26
|
+
- **Prompt and completion text** are never logged, exported or stored by default. Events
|
|
27
|
+
contain a SHA-256 hash and the length. Raw text only appears in events with
|
|
28
|
+
`captureContent: true`, and is never written to the database.
|
|
29
|
+
- **User ids** are recorded as a salted hash (`metrics.userHashSalt`; set it from the
|
|
30
|
+
environment, not in committed files).
|
|
31
|
+
- **Credentials** are never read or stored by this package. AI Core credentials are handled
|
|
32
|
+
by the SAP Cloud SDK for AI; destination credentials by the SAP Cloud SDK. Don't put
|
|
33
|
+
secrets in `metrics.http.headers` in committed files.
|
|
34
|
+
- **The response cache** is in memory per app instance. By default it is scoped per tenant,
|
|
35
|
+
so cached answers are never shared across tenants (`cache.scope`).
|
|
36
|
+
- **The admin service** is read-only and requires a role (`admin.role`).
|
|
37
|
+
- **Published package contents** are checked before every publish (`npm run check:pack`):
|
|
38
|
+
no tests, `.env`, `.cdsrc-private.json`, service keys, certificates or benchmark results.
|
|
39
|
+
Releases are published from GitHub Actions with npm provenance.
|
package/cds-plugin.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
/**
|
|
3
|
+
* CAP plugin, loaded automatically by @sap/cds when this package is a dependency.
|
|
4
|
+
* Reads cds.requires.tokenOptimizer, starts the exporters and flushes them on shutdown.
|
|
5
|
+
*/
|
|
6
|
+
const cds = globalThis.cds ?? require('@sap/cds')
|
|
7
|
+
const { optimizer, countText } = require('./index')
|
|
8
|
+
const log = require('./lib/util/log')
|
|
9
|
+
const path = require('node:path')
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
optimizer.configure(cds.env.requires?.tokenOptimizer)
|
|
13
|
+
log.debug('configured, mode =', optimizer.config.mode, 'exporters =', optimizer.config.metrics.exporters)
|
|
14
|
+
setupModels(optimizer.config)
|
|
15
|
+
} catch (e) {
|
|
16
|
+
log.error('could not configure token optimizer:', e?.message)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Add our CDS models to the app's model when the features using them are enabled:
|
|
21
|
+
* the TokenUsageLog entity for the db exporter, the read-only admin service, and the
|
|
22
|
+
* UsageLog projection when both are on. Plugins load before the model, so adding a `model`
|
|
23
|
+
* to our cds.requires entry is enough.
|
|
24
|
+
*/
|
|
25
|
+
function setupModels (cfg) {
|
|
26
|
+
const exporters = cfg.metrics.exporters ?? []
|
|
27
|
+
const dbLog = exporters.includes('db') || exporters.includes('hana')
|
|
28
|
+
const admin = cfg.admin?.enabled
|
|
29
|
+
const models = []
|
|
30
|
+
if (dbLog) models.push('db/token-usage-log')
|
|
31
|
+
if (admin) models.push('srv/admin-service')
|
|
32
|
+
if (admin && dbLog) models.push('srv/admin-usage-log')
|
|
33
|
+
if (!models.length) return
|
|
34
|
+
|
|
35
|
+
const req = cds.env.requires.tokenOptimizer ??= {}
|
|
36
|
+
req.model = [...new Set([].concat(req.model ?? [], models.map(m => path.join(__dirname, m))))]
|
|
37
|
+
if (!admin) return
|
|
38
|
+
const role = cfg.admin.role || 'TokenOptimizerAdmin'
|
|
39
|
+
cds.on('loaded', (csn) => {
|
|
40
|
+
const srv = csn?.definitions?.TokenOptimizerService
|
|
41
|
+
if (srv) srv['@requires'] = role
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Load the tokenizer tables once the server is up, so the first AI call doesn't pay for it.
|
|
46
|
+
cds.once('served', () => setImmediate(() => {
|
|
47
|
+
try { countText('warm up', 'gpt-4o') } catch { /* estimation falls back to the heuristic */ }
|
|
48
|
+
}))
|
|
49
|
+
|
|
50
|
+
cds.on('shutdown', () => optimizer.shutdown())
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
namespace cap.ai.token_optimizer;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One row per AI Core call, written by the `db` metrics exporter (alias `hana`).
|
|
5
|
+
* Contains metrics only: never prompt or completion text. In multitenant apps each tenant's
|
|
6
|
+
* calls are written to that tenant's database container.
|
|
7
|
+
*/
|
|
8
|
+
entity TokenUsageLog {
|
|
9
|
+
key ID : UUID;
|
|
10
|
+
timestamp : Timestamp;
|
|
11
|
+
tenant : String(255);
|
|
12
|
+
userHash : String(16);
|
|
13
|
+
cdsService : String(255);
|
|
14
|
+
useCase : String(255);
|
|
15
|
+
mode : String(10);
|
|
16
|
+
model : String(255);
|
|
17
|
+
deploymentId : String(255);
|
|
18
|
+
resourceGroup : String(255);
|
|
19
|
+
sdk : String(30);
|
|
20
|
+
operation : String(10);
|
|
21
|
+
requestId : String(255);
|
|
22
|
+
finishReason : String(50);
|
|
23
|
+
promptEstimatedBefore : Integer;
|
|
24
|
+
promptEstimatedAfter : Integer;
|
|
25
|
+
promptTokens : Integer;
|
|
26
|
+
completionTokens : Integer;
|
|
27
|
+
totalTokens : Integer;
|
|
28
|
+
cachedTokens : Integer;
|
|
29
|
+
savedTokensEstimate : Integer;
|
|
30
|
+
/** JSON object: estimated tokens saved per step */
|
|
31
|
+
savingsByStep : LargeString;
|
|
32
|
+
cacheHit : Boolean;
|
|
33
|
+
promptPrefixChanged : Boolean;
|
|
34
|
+
costEstimate : Decimal(20, 6);
|
|
35
|
+
latencyMs : Integer;
|
|
36
|
+
retries : Integer;
|
|
37
|
+
status : String(10);
|
|
38
|
+
errorCategory : String(30);
|
|
39
|
+
errorHttpStatus : Integer;
|
|
40
|
+
errorCode : String(100);
|
|
41
|
+
errorMessage : String(300);
|
|
42
|
+
promptHash : String(64);
|
|
43
|
+
promptLength : Integer;
|
|
44
|
+
}
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
# Add @viveka10/cap-ai-token-optimizer to an existing CAP app on SAP BTP
|
|
2
|
+
|
|
3
|
+
This guide is for a CAP Node.js app on SAP BTP Cloud Foundry that already calls SAP AI Core
|
|
4
|
+
through the SAP Cloud SDK for AI (`@sap-ai-sdk/*`) with an **AI Core service binding**. It
|
|
5
|
+
covers the changes to the app, deployment (`mta.yaml`, `xs-security.json`) and SAP Cloud ALM.
|
|
6
|
+
|
|
7
|
+
**What stays the same**
|
|
8
|
+
|
|
9
|
+
- **AI Core binding:** no change. The SDK keeps reading it from `VCAP_SERVICES`.
|
|
10
|
+
- **AI code:** no change needed. Automatic instrumentation measures the existing calls.
|
|
11
|
+
- **Prompts:** you start in `shadow` mode, which sends your original prompts unchanged.
|
|
12
|
+
|
|
13
|
+
**Contents**
|
|
14
|
+
|
|
15
|
+
1. [Check the prerequisites](#1-check-the-prerequisites)
|
|
16
|
+
2. [Install the package](#2-install-the-package)
|
|
17
|
+
3. [Configure it](#3-configure-it)
|
|
18
|
+
4. [Code changes (optional)](#4-code-changes-optional)
|
|
19
|
+
5. [Local testing with the AI Core binding](#5-local-testing-with-the-ai-core-binding)
|
|
20
|
+
6. [Secrets and environment](#6-secrets-and-environment)
|
|
21
|
+
7. [Usage log in SAP HANA (optional)](#7-usage-log-in-sap-hana-optional)
|
|
22
|
+
8. [Admin service and roles (optional)](#8-admin-service-and-roles-optional)
|
|
23
|
+
9. [SAP Cloud ALM](#9-sap-cloud-alm)
|
|
24
|
+
10. [Deploy and verify](#10-deploy-and-verify)
|
|
25
|
+
11. [Switch from shadow to active](#11-switch-from-shadow-to-active)
|
|
26
|
+
12. [Turn it off or roll back](#12-turn-it-off-or-roll-back)
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 1. Check the prerequisites
|
|
31
|
+
|
|
32
|
+
In the app's root folder:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
node --version
|
|
36
|
+
npm ls @sap/cds @sap-ai-sdk/orchestration @sap-ai-sdk/foundation-models @sap-ai-sdk/langchain
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
| Requirement | Why |
|
|
40
|
+
|---|---|
|
|
41
|
+
| Node.js **22.12 or later**, locally and on Cloud Foundry | The SAP Cloud SDK for AI is ESM-only; Node 22.12+ can load it from CommonJS. |
|
|
42
|
+
| `"engines": { "node": ">=22.12" }` in the app's `package.json` | The Cloud Foundry Node.js buildpack picks the Node version from `engines`. |
|
|
43
|
+
| `@sap/cds` 9 or 10 | Tested versions. |
|
|
44
|
+
| `@sap-ai-sdk/*` **2.8 or later**, all packages on the **same** version, each listed **once** | The packages share internal APIs. Automatic instrumentation only hooks the copy resolved from the app root. If `npm ls` shows nested copies, run `npm dedupe` or align the versions. |
|
|
45
|
+
|
|
46
|
+
## 2. Install the package
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
npm add @viveka10/cap-ai-token-optimizer
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
It is a CAP plugin, so there's nothing to register: CAP loads it at startup.
|
|
53
|
+
|
|
54
|
+
## 3. Configure it
|
|
55
|
+
|
|
56
|
+
Add the `tokenOptimizer` block to `cds.requires` in the app's `package.json` (or `.cdsrc.json`).
|
|
57
|
+
This starting configuration measures everything without changing any AI call:
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"cds": {
|
|
62
|
+
"requires": {
|
|
63
|
+
"tokenOptimizer": {
|
|
64
|
+
"mode": "shadow",
|
|
65
|
+
"autoInstrument": true,
|
|
66
|
+
"pricing": {
|
|
67
|
+
"gpt-4.1": { "inputPer1k": 0.0, "outputPer1k": 0.0 }
|
|
68
|
+
},
|
|
69
|
+
"metrics": { "exporters": ["console"] },
|
|
70
|
+
"[production]": {
|
|
71
|
+
"metrics": { "exporters": ["console", "otel"] },
|
|
72
|
+
"admin": { "enabled": true }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
| Setting | What it does |
|
|
81
|
+
|---|---|
|
|
82
|
+
| `mode: "shadow"` | Runs the optimizer and records what it *would* save, but sends the original prompts. |
|
|
83
|
+
| `autoInstrument: true` | Hooks the SDK's `OrchestrationClient` and `AzureOpenAiChatClient`, so existing calls are measured without code changes. |
|
|
84
|
+
| `pricing` | Price per 1k tokens per model, from your SAP contract. Leave it out if you don't need cost estimates. |
|
|
85
|
+
| `metrics.exporters` | Where events go. `console` writes one JSON line per AI call to the app log (`cf logs`). `otel` sends OpenTelemetry metrics and spans (see [SAP Cloud ALM](#9-sap-cloud-alm)). `db` writes a usage table (see [step 7](#7-usage-log-in-sap-hana-optional)). |
|
|
86
|
+
| `admin.enabled` | Serves the read-only statistics service at `/odata/v4/token-optimizer` (see [step 8](#8-admin-service-and-roles-optional)). |
|
|
87
|
+
|
|
88
|
+
All options are described in the [configuration reference](https://github.com/viveka10/npm_token_optimizer/blob/main/cap-ai-token-optimizer/README.md#configuration-reference).
|
|
89
|
+
|
|
90
|
+
## 4. Code changes (optional)
|
|
91
|
+
|
|
92
|
+
With `autoInstrument: true`, no code change is needed. All hooked calls are recorded under
|
|
93
|
+
the use case `default`. These optional changes give you better reporting and per-use-case
|
|
94
|
+
settings:
|
|
95
|
+
|
|
96
|
+
**Name the use cases** where your handlers call AI Core:
|
|
97
|
+
|
|
98
|
+
```js
|
|
99
|
+
const { optimizer } = require('@viveka10/cap-ai-token-optimizer')
|
|
100
|
+
|
|
101
|
+
this.on('summarizeTicket', req =>
|
|
102
|
+
optimizer.withUseCase('ticket-summary', () => this.summarize(req.data.ID)))
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**LangChain models** from `@sap-ai-sdk/langchain`: wrap them once where you create them:
|
|
106
|
+
|
|
107
|
+
```js
|
|
108
|
+
const model = optimizer.wrapLangChain(new OrchestrationClient(config), { useCase: 'support-agent' })
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Better error responses** (optional): the SDK's errors are passed through unchanged, and
|
|
112
|
+
`classifyError` tells you what happened:
|
|
113
|
+
|
|
114
|
+
```js
|
|
115
|
+
const { classifyError } = require('@viveka10/cap-ai-token-optimizer')
|
|
116
|
+
try { /* AI call */ } catch (err) {
|
|
117
|
+
const { category } = classifyError(err) // RATE_LIMIT, CONTENT_FILTER, CONTEXT_LENGTH, TIMEOUT, …
|
|
118
|
+
return req.reject(category === 'RATE_LIMIT' ? 429 : 502, `AI service error: ${category}`)
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Use cases can then get their own settings, for example
|
|
123
|
+
`"useCases": { "ticket-summary": { "steps": { "outputCap": { "default": 300 } } } }`.
|
|
124
|
+
|
|
125
|
+
## 5. Local testing with the AI Core binding
|
|
126
|
+
|
|
127
|
+
Nothing changes in how the app gets its AI Core credentials. The SDK reads the `aicore` binding
|
|
128
|
+
from `VCAP_SERVICES`. If you already test locally with `cds bind`, keep doing that:
|
|
129
|
+
|
|
130
|
+
```sh
|
|
131
|
+
cf login …
|
|
132
|
+
cds bind aicore --to <your-aicore-instance>:<service-key> # once; writes .cdsrc-private.json (keep it out of git)
|
|
133
|
+
cds bind --exec -- cds watch --profile hybrid # VCAP_SERVICES is set for the SDK
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Call the feature that uses AI Core. The console shows one line per call:
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{"type":"ai-core-call","useCase":"default","mode":"shadow","model":"gpt-4.1",
|
|
140
|
+
"tokens":{"promptEstimatedBefore":2015,"promptEstimatedAfter":805,"prompt":2010,"completion":41,…},
|
|
141
|
+
"savings":{"estimatedTokens":1210,"byStep":{"history":774,"dedupe":340,…},…},"status":"success",…}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`savings` shows what `active` mode would have saved on that call.
|
|
145
|
+
|
|
146
|
+
## 6. Secrets and environment
|
|
147
|
+
|
|
148
|
+
**User-id hash salt.** User ids are recorded as a hash. Set a random salt per landscape, but
|
|
149
|
+
**not** in `package.json` or `mta.yaml` if they are in git. Use an MTA extension descriptor that
|
|
150
|
+
you keep out of git:
|
|
151
|
+
|
|
152
|
+
```yaml
|
|
153
|
+
# secrets.mtaext (do not commit)
|
|
154
|
+
_schema-version: '3.1'
|
|
155
|
+
ID: my-cap-app.secrets
|
|
156
|
+
extends: my-cap-app
|
|
157
|
+
modules:
|
|
158
|
+
- name: my-cap-app-srv
|
|
159
|
+
properties:
|
|
160
|
+
cds_requires_tokenOptimizer_metrics_userHashSalt: <long random string>
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
```sh
|
|
164
|
+
cf deploy mta_archives/my-cap-app_1.0.0.mtar -e secrets.mtaext
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**Overriding settings without a rebuild.** Any setting can be overridden with an environment
|
|
168
|
+
variable in the same format, for example `cds_requires_tokenOptimizer_mode=off`, followed by
|
|
169
|
+
`cf restart`.
|
|
170
|
+
|
|
171
|
+
Never set `captureContent: true` in production. It adds prompt and answer text to events.
|
|
172
|
+
|
|
173
|
+
## 7. Usage log in SAP HANA (optional)
|
|
174
|
+
|
|
175
|
+
To keep one row per AI call in the app's database (for reporting or your own analytics), add
|
|
176
|
+
the `db` exporter:
|
|
177
|
+
|
|
178
|
+
```json
|
|
179
|
+
"[production]": { "metrics": { "exporters": ["console", "otel", "db"] }, "admin": { "enabled": true } }
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
- The plugin adds the entity `cap.ai.token_optimizer.TokenUsageLog` to your model. It is
|
|
183
|
+
deployed with your normal HANA deployment (the `db-deployer` module in the MTA), so there's
|
|
184
|
+
no extra step.
|
|
185
|
+
- **Multitenant apps (MTX):** each tenant's rows go to that tenant's container. Run your
|
|
186
|
+
usual tenant upgrade after deploying, so existing tenants get the new table.
|
|
187
|
+
- Rows older than 30 days are deleted (`metrics.db.retentionDays`). No prompt or answer text
|
|
188
|
+
is stored.
|
|
189
|
+
- With the admin service enabled, the rows are readable at `/odata/v4/token-optimizer/UsageLog`.
|
|
190
|
+
- To expose the table in your own service:
|
|
191
|
+
```cds
|
|
192
|
+
using { cap.ai.token_optimizer.TokenUsageLog } from '@viveka10/cap-ai-token-optimizer/db/token-usage-log';
|
|
193
|
+
service ReportingService { @readonly entity AiUsage as projection on TokenUsageLog; }
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
> The usage log has been tested with SQLite. It hasn't been tested on SAP HANA or with MTX
|
|
197
|
+
> yet, so check it in a development space first.
|
|
198
|
+
|
|
199
|
+
## 8. Admin service and roles (optional)
|
|
200
|
+
|
|
201
|
+
With `admin.enabled`, the app serves `/odata/v4/token-optimizer`:
|
|
202
|
+
|
|
203
|
+
| Path | Content |
|
|
204
|
+
|---|---|
|
|
205
|
+
| `summary()` | totals, mode, latency percentiles |
|
|
206
|
+
| `UseCases`, `Models`, `ErrorCategories`, `StepSavings`, `PromptPrefixChanges`, `Exporters` | statistics of the app instance that answers (in memory, since its start) |
|
|
207
|
+
| `UsageLog` | the usage table, when the `db` exporter is on |
|
|
208
|
+
|
|
209
|
+
It requires the role `TokenOptimizerAdmin` (configurable with `admin.role`). Add it to
|
|
210
|
+
`xs-security.json`:
|
|
211
|
+
|
|
212
|
+
```json
|
|
213
|
+
{
|
|
214
|
+
"scopes": [
|
|
215
|
+
{ "name": "$XSAPPNAME.TokenOptimizerAdmin", "description": "Read AI token usage statistics" }
|
|
216
|
+
],
|
|
217
|
+
"role-templates": [
|
|
218
|
+
{ "name": "TokenOptimizerAdmin", "scope-references": ["$XSAPPNAME.TokenOptimizerAdmin"] }
|
|
219
|
+
],
|
|
220
|
+
"role-collections": [
|
|
221
|
+
{ "name": "AI_Token_Optimizer_Admin", "role-template-references": ["$XSAPPNAME.TokenOptimizerAdmin"] }
|
|
222
|
+
]
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Merge this into your existing file; don't replace it. After deploying, assign the role
|
|
227
|
+
collection to the right users in the BTP cockpit (Security → Role Collections).
|
|
228
|
+
|
|
229
|
+
## 9. SAP Cloud ALM
|
|
230
|
+
|
|
231
|
+
The optimizer has no Cloud ALM-specific code. Its `otel` exporter writes OpenTelemetry metrics
|
|
232
|
+
and one span per AI call through whatever OpenTelemetry setup the app has. For Cloud ALM, that
|
|
233
|
+
setup is SAP's Cloud ALM agent plus `@cap-js/telemetry`. The agent setup below follows SAP's
|
|
234
|
+
documentation:
|
|
235
|
+
[Data Collection Infrastructure](https://support.sap.com/en/alm/sap-cloud-alm/operations/expert-portal/data-collection-infrastructure.html) and
|
|
236
|
+
[Auto-instrumentation for Node.js](https://support.sap.com/en/alm/sap-cloud-alm/operations/expert-portal/data-collection-infrastructure/autoinstrumentation-nodejs.html).
|
|
237
|
+
Check those pages for changes before you start.
|
|
238
|
+
|
|
239
|
+
### 9.1 Prerequisites
|
|
240
|
+
|
|
241
|
+
- An SAP Cloud ALM tenant, with the **SAP Cloud ALM API** enabled and a service key for it
|
|
242
|
+
([Enabling SAP Cloud ALM API](https://help.sap.com/docs/cloud-alm/setup-administration/enabling-sap-cloud-alm-api)).
|
|
243
|
+
- A technical user for SAP's **Repository Based Shipment Channel** (RBSC), which distributes
|
|
244
|
+
the agent. Request it at https://ui.repositories.cloud.sap/www/webapp/users. The agent isn't
|
|
245
|
+
on the public npm registry.
|
|
246
|
+
|
|
247
|
+
### 9.2 Get access to SAP's package repository
|
|
248
|
+
|
|
249
|
+
Set `SAP_NPM_AUTH` to the "NPM Base64 Credentials" of the technical user (`base64(user:password)`),
|
|
250
|
+
in your shell and in your CI/build pipeline (as a secret), and add to the app's `.npmrc`:
|
|
251
|
+
|
|
252
|
+
```
|
|
253
|
+
//73555000100200018064.npmsrv.cdn.repositories.cloud.sap/:_auth=${SAP_NPM_AUTH}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### 9.3 Add the agent and @cap-js/telemetry
|
|
257
|
+
|
|
258
|
+
In the app's `package.json`:
|
|
259
|
+
|
|
260
|
+
```json
|
|
261
|
+
{
|
|
262
|
+
"dependencies": {
|
|
263
|
+
"@sap/xotel-agent-ext-js": "https://73555000100200018064.npmsrv.cdn.repositories.cloud.sap/@sap/xotel-agent-ext-js/-/xotel-agent-ext-js-2.0.4.tgz",
|
|
264
|
+
"@cap-js/telemetry": "^2.1.0"
|
|
265
|
+
},
|
|
266
|
+
"scripts": {
|
|
267
|
+
"start": "node ${NODE_ARGS} ./node_modules/.bin/cds-serve"
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
- **Agent version:** 2.0.4 or later. That is the version from which `@cap-js/telemetry`
|
|
273
|
+
supports it, and it uses OpenTelemetry 2.x.
|
|
274
|
+
- **`@cap-js/telemetry` version:** 2.1.0 or later. It detects the agent and adds its span
|
|
275
|
+
processor, metric reader and log processor to the agent's pipeline instead of starting a
|
|
276
|
+
second OpenTelemetry SDK. Version 2.0.x doesn't have this integration.
|
|
277
|
+
- **Start script:** loads the agent before CAP via `NODE_ARGS` (set in `mta.yaml` below).
|
|
278
|
+
After `cds build`, check that `gen/srv/package.json` contains this start script.
|
|
279
|
+
- **Dependency conflicts:** SAP notes that the agent's OpenTelemetry 2.x libraries can
|
|
280
|
+
conflict with other OpenTelemetry packages in the app. `@viveka10/cap-ai-token-optimizer`
|
|
281
|
+
itself only uses `@opentelemetry/api`.
|
|
282
|
+
|
|
283
|
+
### 9.4 Enable the otel exporter
|
|
284
|
+
|
|
285
|
+
```json
|
|
286
|
+
"tokenOptimizer": { "[production]": { "metrics": { "exporters": ["console", "otel"] } } }
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### 9.5 mta.yaml
|
|
290
|
+
|
|
291
|
+
```yaml
|
|
292
|
+
modules:
|
|
293
|
+
- name: my-cap-app-srv
|
|
294
|
+
type: nodejs
|
|
295
|
+
path: gen/srv
|
|
296
|
+
properties:
|
|
297
|
+
SAP_CALM_SERVICE_NAME: my-cap-app-srv # name shown in Cloud ALM
|
|
298
|
+
SAP_CALM_SERVICE_TYPE: SAP_CP_CF
|
|
299
|
+
NODE_ARGS: -r @sap/xotel-agent-ext-js
|
|
300
|
+
requires:
|
|
301
|
+
- name: my-cap-app-aicore # existing AI Core binding, unchanged
|
|
302
|
+
- name: my-cap-app-auth # existing XSUAA
|
|
303
|
+
- name: my-cap-app-db # existing, only needed for the db exporter
|
|
304
|
+
- name: my-cap-app-destination # new: used by the Cloud ALM agent
|
|
305
|
+
|
|
306
|
+
resources:
|
|
307
|
+
- name: my-cap-app-destination
|
|
308
|
+
type: org.cloudfoundry.managed-service
|
|
309
|
+
parameters:
|
|
310
|
+
service: destination
|
|
311
|
+
service-plan: lite
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
**Staging needs the agent's repository.** Cloud Foundry installs dependencies during staging,
|
|
315
|
+
so it has to reach SAP's repository too. SAP suggests, if staging fails, shipping
|
|
316
|
+
`node_modules` and `package-lock.json` inside the MTA archive: remove them from the module's
|
|
317
|
+
`build-parameters.ignore` list, and install them into `gen/srv` with a `before-all` custom
|
|
318
|
+
build step (`npm install --production` and `npm install --prefix ./gen/srv`).
|
|
319
|
+
|
|
320
|
+
### 9.6 Destination for Cloud ALM
|
|
321
|
+
|
|
322
|
+
In the **provider subaccount** (BTP cockpit → Connectivity → Destinations), create:
|
|
323
|
+
|
|
324
|
+
| Field | Value |
|
|
325
|
+
|---|---|
|
|
326
|
+
| Name | `CALM_datacollector` (exactly this name) |
|
|
327
|
+
| Type | HTTP |
|
|
328
|
+
| URL | the `Api` URL from the Cloud ALM API service key, e.g. `https://eu10.alm.cloud.sap/api` |
|
|
329
|
+
| Proxy Type | Internet |
|
|
330
|
+
| Authentication | OAuth2ClientCredentials |
|
|
331
|
+
| Client ID / Client Secret | from the Cloud ALM API service key |
|
|
332
|
+
| Token Service URL | the `url` from the service key |
|
|
333
|
+
|
|
334
|
+
SAP notes that "Check Connection" can return 404 even when the destination is correct.
|
|
335
|
+
|
|
336
|
+
### 9.7 Activate monitoring in Cloud ALM
|
|
337
|
+
|
|
338
|
+
In SAP Cloud ALM, activate the monitoring use cases you need for the service (for example
|
|
339
|
+
Health Monitoring and Exception Monitoring). Optional agent settings and how to turn the agent
|
|
340
|
+
off (`SAP_CALM_INSTRUMENTATION_ENABLED=false`) are on SAP's auto-instrumentation page.
|
|
341
|
+
|
|
342
|
+
### 9.8 What you'll see, and what isn't verified
|
|
343
|
+
|
|
344
|
+
- **App log after deploying:** a line from `@cap-js/telemetry` saying it found
|
|
345
|
+
`@sap/xotel-agent-ext-js` and added its span processor as a delegate. The optimizer logs a
|
|
346
|
+
warning if no OpenTelemetry metrics provider is registered.
|
|
347
|
+
- **Spans:** the optimizer's `chat <model>` spans are children of the CAP request spans and
|
|
348
|
+
carry token usage, savings and error category, never prompt text. They go through the same
|
|
349
|
+
pipeline as the CAP spans.
|
|
350
|
+
- **Not documented by SAP:** whether Cloud ALM shows **custom** OpenTelemetry metrics (like
|
|
351
|
+
`gen_ai.client.token.usage` or `cap_ai_optimizer.tokens.saved`) in its UIs. Its documented
|
|
352
|
+
use cases are Health, Exception, Job & Automation, Business Process and Real User Monitoring.
|
|
353
|
+
- **Not verified:** this setup hasn't been tested against a Cloud ALM tenant.
|
|
354
|
+
|
|
355
|
+
If you need token dashboards, use the `db` exporter with the admin service or your own
|
|
356
|
+
reporting service, or send the `otel` data to SAP Cloud Logging or Dynatrace via
|
|
357
|
+
`@cap-js/telemetry` (`cds.requires.telemetry.kind` = `to-cloud-logging` / `to-dynatrace`).
|
|
358
|
+
Note that with the agent present, `@cap-js/telemetry` also exports through its own
|
|
359
|
+
configured kind (console by default), so check the log volume after deploying.
|
|
360
|
+
|
|
361
|
+
## 10. Deploy and verify
|
|
362
|
+
|
|
363
|
+
```sh
|
|
364
|
+
npm install # updates package-lock.json
|
|
365
|
+
mbt build
|
|
366
|
+
cf deploy mta_archives/<your-app>_<version>.mtar -e secrets.mtaext
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Then check:
|
|
370
|
+
|
|
371
|
+
1. **Logs:** `cf logs my-cap-app-srv --recent` shows
|
|
372
|
+
`autoInstrument: OrchestrationClient … calls are optimized and measured` at startup, and one
|
|
373
|
+
`"type":"ai-core-call"` line per AI call.
|
|
374
|
+
2. **Admin service:** with the `TokenOptimizerAdmin` role, open
|
|
375
|
+
`https://<app-url>/odata/v4/token-optimizer/summary()`.
|
|
376
|
+
3. **Cloud ALM:** the service appears under `SAP_CALM_SERVICE_NAME` in the activated
|
|
377
|
+
monitoring apps.
|
|
378
|
+
4. **Behavior:** your AI features answer exactly as before (shadow mode sends the original prompts).
|
|
379
|
+
|
|
380
|
+
## 11. Switch from shadow to active
|
|
381
|
+
|
|
382
|
+
After a few days in shadow mode, compare `savings.estimatedTokens` and the notes in the events
|
|
383
|
+
or the admin service per use case. Then switch one use case at a time:
|
|
384
|
+
|
|
385
|
+
```json
|
|
386
|
+
"tokenOptimizer": {
|
|
387
|
+
"mode": "shadow",
|
|
388
|
+
"useCases": {
|
|
389
|
+
"ticket-summary": { "mode": "active" }
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
In `active` mode the optimized prompts are sent, and identical calls with `temperature` ≤ 0.2
|
|
395
|
+
are answered from the cache, which is per app instance and per tenant. Check answer quality
|
|
396
|
+
for that use case before switching the next one. If an agent needs more context or tools,
|
|
397
|
+
raise `steps.history.maxTurns` or `steps.tools.maxTools`, or pin the tools it needs, for that
|
|
398
|
+
use case.
|
|
399
|
+
|
|
400
|
+
## 12. Turn it off or roll back
|
|
401
|
+
|
|
402
|
+
| Goal | How |
|
|
403
|
+
|---|---|
|
|
404
|
+
| Stop optimizing, keep measuring | `cds_requires_tokenOptimizer_mode=off`, then `cf restart` |
|
|
405
|
+
| Stop hooking the SDK | `cds_requires_tokenOptimizer_autoInstrument=false`, then `cf restart` |
|
|
406
|
+
| Turn off the Cloud ALM agent | `SAP_CALM_INSTRUMENTATION_ENABLED=false`, then `cf restart` |
|
|
407
|
+
| Remove the package | `npm rm @viveka10/cap-ai-token-optimizer`, delete the `tokenOptimizer` block, redeploy. If you used the `db` exporter, the `TokenUsageLog` table stays until you drop it. |
|
|
408
|
+
|
|
409
|
+
## Checklist
|
|
410
|
+
|
|
411
|
+
- [ ] Node ≥ 22.12 and `engines` set; `@sap-ai-sdk/*` ≥ 2.8, same version, no duplicates
|
|
412
|
+
- [ ] `npm add @viveka10/cap-ai-token-optimizer`
|
|
413
|
+
- [ ] `tokenOptimizer` block with `mode: "shadow"` and `autoInstrument: true`; `pricing` filled in
|
|
414
|
+
- [ ] Salt and other secrets in an `.mtaext` that is not committed
|
|
415
|
+
- [ ] Optional: `withUseCase` / `wrapLangChain` for named use cases
|
|
416
|
+
- [ ] Optional: `db` exporter (HANA table deployed with the app; MTX upgrade)
|
|
417
|
+
- [ ] Optional: admin service plus `TokenOptimizerAdmin` role collection
|
|
418
|
+
- [ ] Cloud ALM:
|
|
419
|
+
- [ ] RBSC user and `SAP_NPM_AUTH`
|
|
420
|
+
- [ ] agent ≥ 2.0.4 and `@cap-js/telemetry` ≥ 2.1.0
|
|
421
|
+
- [ ] `NODE_ARGS`, `SAP_CALM_*` properties and destination service in `mta.yaml`
|
|
422
|
+
- [ ] `CALM_datacollector` destination
|
|
423
|
+
- [ ] monitoring use cases activated in Cloud ALM
|
|
424
|
+
- [ ] `otel` exporter enabled
|
|
425
|
+
- [ ] Deploy, check logs and the admin service, then switch use cases to `active` one by one
|