@viveka10/cap-ai-token-optimizer 0.0.0-stage → 0.1.0

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.
@@ -0,0 +1,163 @@
1
+ 'use strict'
2
+ /**
3
+ * Pre-call token estimation.
4
+ * Uses js-tiktoken for OpenAI model families and a chars/4 heuristic for everything else
5
+ * (Anthropic, Gemini, Mistral, … tokenizers are not public / not bundled).
6
+ * Estimates are approximate; post-call metrics prefer real usage from the response.
7
+ */
8
+
9
+ // Overhead per message / per reply, following OpenAI's published counting recipe.
10
+ const TOKENS_PER_MESSAGE = 3
11
+ const REPLY_PRIMING = 3
12
+ // Rough flat cost for a non-text content part (e.g. an image) when estimating.
13
+ const NON_TEXT_PART_TOKENS = 85
14
+
15
+ const FAMILIES = [
16
+ // order matters: more specific prefixes first
17
+ { re: /^(gpt-4o|gpt-4\.1|gpt-4\.5|gpt-5|gpt-oss|o1|o3|o4|chatgpt-4o|gpt-realtime)/, encoding: 'o200k_base' },
18
+ { re: /^(gpt-4|gpt-3\.5|text-embedding-3|text-embedding-ada)/, encoding: 'cl100k_base' }
19
+ ]
20
+
21
+ const encoders = new Map()
22
+ let tiktoken // lazy: the rank tables are large
23
+
24
+ /**
25
+ * Strip vendor prefixes used by AI Core model names, e.g. "azure-openai--gpt-4.1" or "openai/gpt-4.1".
26
+ * @param {string} model
27
+ */
28
+ const baseModelName = (model) => String(model ?? '').toLowerCase().replace(/^.*(--|\/)/, '')
29
+
30
+ /**
31
+ * @param {string} [model]
32
+ * @returns {string|null} tiktoken encoding name, or null when the heuristic should be used
33
+ */
34
+ function encodingFor (model) {
35
+ const name = baseModelName(model)
36
+ return FAMILIES.find(f => f.re.test(name))?.encoding ?? null
37
+ }
38
+
39
+ function getEncoder (encoding) {
40
+ if (!encoders.has(encoding)) {
41
+ tiktoken ??= require('js-tiktoken')
42
+ encoders.set(encoding, tiktoken.getEncoding(encoding))
43
+ }
44
+ return encoders.get(encoding)
45
+ }
46
+
47
+ /** @param {string} text */
48
+ const heuristic = (text) => Math.ceil(text.length / 4)
49
+
50
+ /**
51
+ * Count tokens of a string.
52
+ * @param {string} text
53
+ * @param {string} [model]
54
+ * @returns {{ tokens: number, method: 'tiktoken'|'heuristic' }}
55
+ */
56
+ function countText (text, model) {
57
+ const str = typeof text === 'string' ? text : String(text ?? '')
58
+ if (!str) return { tokens: 0, method: encodingFor(model) ? 'tiktoken' : 'heuristic' }
59
+ const encoding = encodingFor(model)
60
+ if (encoding) {
61
+ try {
62
+ return { tokens: getEncoder(encoding).encode(str).length, method: 'tiktoken' }
63
+ } catch { /* fall through to heuristic */ }
64
+ }
65
+ return { tokens: heuristic(str), method: 'heuristic' }
66
+ }
67
+
68
+ /**
69
+ * Flatten message content into text and count non-text parts.
70
+ * @param {any} content
71
+ * @returns {{ text: string, extraTokens: number }}
72
+ */
73
+ function contentText (content) {
74
+ if (content == null) return { text: '', extraTokens: 0 }
75
+ if (typeof content === 'string') return { text: content, extraTokens: 0 }
76
+ if (Array.isArray(content)) {
77
+ let text = ''
78
+ let extraTokens = 0
79
+ for (const part of content) {
80
+ if (typeof part === 'string') text += part
81
+ else if (typeof part?.text === 'string') text += part.text
82
+ else extraTokens += NON_TEXT_PART_TOKENS
83
+ }
84
+ return { text, extraTokens }
85
+ }
86
+ return { text: JSON.stringify(content), extraTokens: 0 }
87
+ }
88
+
89
+ /**
90
+ * Tokens of one message, including the per-message overhead.
91
+ * @param {object} m
92
+ * @param {string} [model]
93
+ */
94
+ function messageTokens (m, model) {
95
+ if (!m) return 0
96
+ const { text, extraTokens } = contentText(m.content)
97
+ const parts = [m.role ?? '', text]
98
+ if (m.name) parts.push(m.name)
99
+ if (m.tool_calls) parts.push(JSON.stringify(m.tool_calls))
100
+ if (m.tool_call_id) parts.push(m.tool_call_id)
101
+ return TOKENS_PER_MESSAGE + countText(parts.join('\n'), model).tokens + extraTokens
102
+ }
103
+
104
+ /**
105
+ * Tokens of one tool definition (serialised JSON; providers use a similar but not
106
+ * identical internal format).
107
+ * @param {object} tool
108
+ * @param {string} [model]
109
+ */
110
+ const toolTokens = (tool, model) => countText(JSON.stringify(tool ?? null), model).tokens
111
+
112
+ /**
113
+ * Estimate the prompt tokens of a chat request.
114
+ * @param {Array<object>} messages
115
+ * @param {{ model?: string, tools?: Array<object> }} [opts]
116
+ * @returns {{ tokens: number, method: 'tiktoken'|'heuristic' }}
117
+ */
118
+ function estimateMessages (messages, { model, tools } = {}) {
119
+ return createMemoEstimator(model).estimate(messages, tools)
120
+ }
121
+
122
+ /**
123
+ * Estimate tokens consumed by tool definitions.
124
+ * @param {Array<object>} tools
125
+ * @param {string} [model]
126
+ */
127
+ function estimateTools (tools, model) {
128
+ const method = encodingFor(model) ? 'tiktoken' : 'heuristic'
129
+ if (!Array.isArray(tools) || !tools.length) return { tokens: 0, method }
130
+ return { tokens: tools.reduce((n, t) => n + toolTokens(t, model), 0), method }
131
+ }
132
+
133
+ /**
134
+ * Estimator that remembers the count of every message and tool object it has seen.
135
+ * Pipeline steps keep unchanged messages by identity, so re-estimating after each
136
+ * step only counts what the step changed.
137
+ * @param {string} [model]
138
+ */
139
+ function createMemoEstimator (model) {
140
+ const method = encodingFor(model) ? 'tiktoken' : 'heuristic'
141
+ const memo = new WeakMap()
142
+ const cached = (obj, fn) => {
143
+ if (obj === null || typeof obj !== 'object') return fn(obj, model)
144
+ if (!memo.has(obj)) memo.set(obj, fn(obj, model))
145
+ return memo.get(obj)
146
+ }
147
+ return {
148
+ /**
149
+ * @param {Array<object>} messages
150
+ * @param {Array<object>} [tools]
151
+ * @returns {{ tokens: number, method: 'tiktoken'|'heuristic' }}
152
+ */
153
+ estimate (messages, tools) {
154
+ let tokens = 0
155
+ for (const m of messages ?? []) tokens += cached(m, messageTokens)
156
+ if (tokens > 0) tokens += REPLY_PRIMING
157
+ if (Array.isArray(tools)) for (const t of tools) tokens += cached(t, toolTokens)
158
+ return { tokens, method }
159
+ }
160
+ }
161
+ }
162
+
163
+ module.exports = { countText, estimateMessages, estimateTools, createMemoEstimator, encodingFor, baseModelName }
@@ -0,0 +1,13 @@
1
+ 'use strict'
2
+ /**
3
+ * Access the application's @sap/cds instance.
4
+ * @sap/cds sets `global.cds`; preferring it avoids picking up a second copy from this
5
+ * package's own node_modules (common with `file:` or `npm link` installs).
6
+ * Returns null when CAP is not loaded — the optimizer also works outside CAP.
7
+ * @returns {any|null}
8
+ */
9
+ function getCds () {
10
+ return globalThis.cds ?? null
11
+ }
12
+
13
+ module.exports = { getCds }
@@ -0,0 +1,40 @@
1
+ 'use strict'
2
+ const { createHash } = require('node:crypto')
3
+
4
+ /** @param {string} text */
5
+ const sha256 = (text) => createHash('sha256').update(text).digest('hex')
6
+
7
+ /**
8
+ * Deterministic JSON serialisation (sorted object keys) so equal values hash equally.
9
+ * @param {any} value
10
+ * @returns {string}
11
+ */
12
+ function stableStringify (value) {
13
+ if (value === undefined) return 'null'
14
+ if (value === null || typeof value !== 'object') return JSON.stringify(value)
15
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`
16
+ const keys = Object.keys(value).filter(k => value[k] !== undefined).sort()
17
+ return `{${keys.map(k => `${JSON.stringify(k)}:${stableStringify(value[k])}`).join(',')}}`
18
+ }
19
+
20
+ /**
21
+ * Pseudonymise a user id. Returns null for empty input.
22
+ * @param {string|undefined|null} userId
23
+ * @param {string} [salt]
24
+ */
25
+ function hashUser (userId, salt = '') {
26
+ if (!userId) return null
27
+ return sha256(`${salt}:${userId}`).slice(0, 16)
28
+ }
29
+
30
+ /**
31
+ * Hash + length fingerprint of a prompt — what gets logged instead of the raw text.
32
+ * @param {Array<object>} messages
33
+ * @returns {{ promptHash: string, promptLength: number }}
34
+ */
35
+ function fingerprint (messages) {
36
+ const text = stableStringify(messages ?? [])
37
+ return { promptHash: sha256(text), promptLength: text.length }
38
+ }
39
+
40
+ module.exports = { sha256, stableStringify, hashUser, fingerprint }
@@ -0,0 +1,37 @@
1
+ 'use strict'
2
+ /**
3
+ * Logger: uses cds.log('token-optimizer') when CAP is loaded, else console.
4
+ * Resolved lazily on first use. Never throws.
5
+ */
6
+ const { getCds } = require('./cds')
7
+
8
+ let _log
9
+
10
+ function getLogger () {
11
+ if (_log) return _log
12
+ const cds = getCds()
13
+ if (cds?.log) _log = cds.log('token-optimizer')
14
+ // the console fallback is not cached, so cds.log is picked up once CAP has loaded
15
+ return _log ?? fallback
16
+ }
17
+
18
+ const fallback = {
19
+ debug: () => {},
20
+ info: (...a) => console.info('[token-optimizer]', ...a),
21
+ warn: (...a) => console.warn('[token-optimizer]', ...a),
22
+ error: (...a) => console.error('[token-optimizer]', ...a)
23
+ }
24
+
25
+ /** @param {'debug'|'info'|'warn'|'error'} level */
26
+ const safe = (level) => (...args) => {
27
+ try { getLogger()[level](...args) } catch { /* logging must never break the caller */ }
28
+ }
29
+
30
+ module.exports = {
31
+ debug: safe('debug'),
32
+ info: safe('info'),
33
+ warn: safe('warn'),
34
+ error: safe('error'),
35
+ /** @internal test hook */
36
+ _set (logger) { _log = logger }
37
+ }
@@ -0,0 +1,55 @@
1
+ 'use strict'
2
+ const { getCds } = require('./cds')
3
+
4
+ /**
5
+ * Load an optional peer dependency (e.g. @sap-ai-sdk/orchestration).
6
+ * Resolves from the application root first so the app's copy is used even when this package
7
+ * is installed via a symlink (`file:` / `npm link`), then falls back to normal resolution.
8
+ * @param {string} name
9
+ * @param {string} [purpose] shown in the error message
10
+ */
11
+ function loadPeer (name, purpose) {
12
+ const roots = [getCds()?.root, process.cwd()].filter(Boolean)
13
+ for (const root of roots) {
14
+ try {
15
+ return require(require.resolve(name, { paths: [root] }))
16
+ } catch (e) {
17
+ if (e?.code !== 'MODULE_NOT_FOUND') throw e
18
+ }
19
+ }
20
+ try {
21
+ return require(name)
22
+ } catch (e) {
23
+ if (e?.code !== 'MODULE_NOT_FOUND') throw e
24
+ const err = new Error(`${purpose ? `${purpose} requires` : 'Missing'} peer dependency "${name}". Install it with: npm i ${name}`)
25
+ err.code = 'PEER_DEPENDENCY_MISSING'
26
+ throw err
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Version of an installed peer, read from its package.json next to the resolved entry
32
+ * (works when package.json is not in the package's "exports"). Null if unknown.
33
+ * @param {string} name
34
+ */
35
+ function peerVersion (name) {
36
+ const fs = require('node:fs')
37
+ const path = require('node:path')
38
+ const roots = [getCds()?.root, process.cwd()].filter(Boolean)
39
+ let entry = null
40
+ for (const root of roots) {
41
+ try { entry = require.resolve(name, { paths: [root] }); break } catch { /* next */ }
42
+ }
43
+ try { entry ??= require.resolve(name) } catch { return null }
44
+ for (let dir = path.dirname(entry); dir !== path.dirname(dir); dir = path.dirname(dir)) {
45
+ const file = path.join(dir, 'package.json')
46
+ if (!fs.existsSync(file)) continue
47
+ try {
48
+ const pkg = JSON.parse(fs.readFileSync(file, 'utf8'))
49
+ if (pkg.name === name) return pkg.version ?? null
50
+ } catch { return null }
51
+ }
52
+ return null
53
+ }
54
+
55
+ module.exports = { loadPeer, peerVersion }
package/package.json CHANGED
@@ -1,6 +1,133 @@
1
1
  {
2
2
  "name": "@viveka10/cap-ai-token-optimizer",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Reduce, measure and report SAP AI Core token usage in SAP CAP Node.js apps: prompt optimization, response cache, metrics for SAP Cloud ALM / OpenTelemetry, HTTP and HANA",
5
+ "keywords": [
6
+ "sap",
7
+ "cap",
8
+ "cds",
9
+ "cds-plugin",
10
+ "sap-ai-core",
11
+ "sap-ai-sdk",
12
+ "generative-ai",
13
+ "llm",
14
+ "tokens",
15
+ "token-optimization",
16
+ "prompt",
17
+ "cache",
18
+ "opentelemetry",
19
+ "cloud-alm",
20
+ "mcp",
21
+ "agents",
22
+ "langchain",
23
+ "btp"
24
+ ],
25
+ "homepage": "https://github.com/viveka10/npm_token_optimizer/tree/main/cap-ai-token-optimizer#readme",
26
+ "bugs": {
27
+ "url": "https://github.com/viveka10/npm_token_optimizer/issues"
28
+ },
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/viveka10/npm_token_optimizer.git",
32
+ "directory": "cap-ai-token-optimizer"
33
+ },
34
+ "license": "MIT",
35
+ "author": "Vivekanandhan Srinivasan",
36
+ "main": "index.js",
37
+ "types": "index.d.ts",
38
+ "exports": {
39
+ ".": {
40
+ "types": "./index.d.ts",
41
+ "default": "./index.js"
42
+ },
43
+ "./cds-plugin": "./cds-plugin.js",
44
+ "./cds-plugin.js": "./cds-plugin.js",
45
+ "./db/*": "./db/*",
46
+ "./srv/*": "./srv/*",
47
+ "./package.json": "./package.json"
48
+ },
49
+ "files": [
50
+ "index.js",
51
+ "index.d.ts",
52
+ "cds-plugin.js",
53
+ "lib/",
54
+ "srv/",
55
+ "db/",
56
+ "docs/",
57
+ "CHANGELOG.md",
58
+ "SECURITY.md"
59
+ ],
60
+ "engines": {
61
+ "node": ">=22.12"
62
+ },
63
+ "publishConfig": {
64
+ "access": "public"
65
+ },
66
+ "scripts": {
67
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
68
+ "test:unit": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/unit",
69
+ "test:integration": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/integration",
70
+ "lint": "eslint .",
71
+ "mock:aicore": "node test/mock-aicore/server.js",
72
+ "bench": "node bench/run.js",
73
+ "test:live": "RUN_LIVE_TESTS=1 node --experimental-vm-modules node_modules/jest/bin/jest.js test/live",
74
+ "test:smoke": "node --test test/smoke/smoke.js",
75
+ "check:pack": "node scripts/check-pack.js",
76
+ "prepublishOnly": "npm run check:pack"
77
+ },
78
+ "cds": {
79
+ "requires": {
80
+ "tokenOptimizer": {
81
+ "kind": "token-optimizer"
82
+ }
83
+ }
84
+ },
85
+ "dependencies": {
86
+ "js-tiktoken": "^1.0.21"
87
+ },
88
+ "peerDependencies": {
89
+ "@sap/cds": "^9 || ^10",
90
+ "@sap-ai-sdk/orchestration": "^2.8.0",
91
+ "@sap-ai-sdk/foundation-models": "^2.8.0",
92
+ "@sap-ai-sdk/langchain": "^2.16.0",
93
+ "@sap-cloud-sdk/connectivity": "^4",
94
+ "@opentelemetry/api": "^1.4"
95
+ },
96
+ "peerDependenciesMeta": {
97
+ "@sap/cds": {
98
+ "optional": true
99
+ },
100
+ "@sap-ai-sdk/orchestration": {
101
+ "optional": true
102
+ },
103
+ "@sap-ai-sdk/foundation-models": {
104
+ "optional": true
105
+ },
106
+ "@sap-ai-sdk/langchain": {
107
+ "optional": true
108
+ },
109
+ "@sap-cloud-sdk/connectivity": {
110
+ "optional": true
111
+ },
112
+ "@opentelemetry/api": {
113
+ "optional": true
114
+ }
115
+ },
116
+ "devDependencies": {
117
+ "@eslint/js": "^9.39.5",
118
+ "@langchain/core": "^1.2.12",
119
+ "@opentelemetry/api": "^1.9.0",
120
+ "@opentelemetry/context-async-hooks": "^2.11.0",
121
+ "@opentelemetry/sdk-metrics": "^2.11.0",
122
+ "@opentelemetry/sdk-trace-base": "^2.11.0",
123
+ "@sap-ai-sdk/foundation-models": "^2.16.0",
124
+ "@sap-ai-sdk/langchain": "^2.16.0",
125
+ "@sap-ai-sdk/orchestration": "^2.16.0",
126
+ "@sap/cds": "^10.1.1",
127
+ "eslint": "^9.39.5",
128
+ "express": "^5.3.0",
129
+ "globals": "^17.13.0",
130
+ "jest": "^30.5.2",
131
+ "langchain": "^1.5.12"
132
+ }
133
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Read-only statistics of @viveka10/cap-ai-token-optimizer for this app instance (in-memory, since
3
+ * start or the last reset). Served at /odata/v4/token-optimizer when
4
+ * cds.requires.tokenOptimizer.admin.enabled is true. The required role is
5
+ * cds.requires.tokenOptimizer.admin.role (default TokenOptimizerAdmin).
6
+ */
7
+ @path : 'token-optimizer'
8
+ @requires: 'TokenOptimizerAdmin'
9
+ @readonly
10
+ service TokenOptimizerService {
11
+
12
+ type Tokens {
13
+ prompt : Integer64;
14
+ completion : Integer64;
15
+ total : Integer64;
16
+ cached : Integer64;
17
+ }
18
+
19
+ type Latency {
20
+ count : Integer;
21
+ avg : Integer;
22
+ p50 : Integer;
23
+ p95 : Integer;
24
+ max : Integer;
25
+ }
26
+
27
+ aspect Totals {
28
+ calls : Integer64;
29
+ success : Integer64;
30
+ errors : Integer64;
31
+ cacheHits : Integer64;
32
+ tokens : Tokens;
33
+ savedTokensEstimate : Integer64;
34
+ costEstimate : Decimal(20, 6);
35
+ }
36
+
37
+ type Summary : Totals {
38
+ since : Timestamp;
39
+ mode : String;
40
+ latencyMs : Latency;
41
+ listenerErrors : Integer;
42
+ }
43
+
44
+ /** Totals for this app instance. */
45
+ function summary() returns Summary;
46
+
47
+ @cds.persistence.skip
48
+ entity UseCases : Totals {
49
+ key name : String;
50
+ }
51
+
52
+ @cds.persistence.skip
53
+ entity Models : Totals {
54
+ key name : String;
55
+ }
56
+
57
+ @cds.persistence.skip
58
+ entity ErrorCategories {
59
+ key category : String;
60
+ count : Integer64;
61
+ }
62
+
63
+ @cds.persistence.skip
64
+ entity StepSavings {
65
+ key step : String;
66
+ tokens : Integer64;
67
+ }
68
+
69
+ /** Prompt-caching advisory: calls whose system prompt or tools changed, per use case. */
70
+ @cds.persistence.skip
71
+ entity PromptPrefixChanges {
72
+ key useCase : String;
73
+ count : Integer64;
74
+ }
75
+
76
+ @cds.persistence.skip
77
+ entity Exporters {
78
+ key name : String;
79
+ errors : Integer64;
80
+ /** JSON from the exporter's stats(), e.g. queued, sent, dropped for http */
81
+ stats : LargeString;
82
+ }
83
+ }
@@ -0,0 +1,41 @@
1
+ 'use strict'
2
+ // Use the app's @sap/cds instance (see lib/util/cds.js)
3
+ const cds = globalThis.cds ?? require('@sap/cds')
4
+ const { optimizer } = require('../index')
5
+
6
+ const totals = (t) => ({
7
+ calls: t.calls,
8
+ success: t.success,
9
+ errors: t.errors,
10
+ cacheHits: t.cacheHits,
11
+ tokens: { ...t.tokens },
12
+ savedTokensEstimate: t.savedTokensEstimate,
13
+ costEstimate: t.costEstimate
14
+ })
15
+
16
+ /** Rows for a READ, honouring a key lookup (e.g. UseCases('ask')). */
17
+ function rows (req, list, key) {
18
+ const wanted = req.data?.[key]
19
+ if (wanted === undefined) return list
20
+ return list.find(r => r[key] === wanted) ?? null
21
+ }
22
+
23
+ module.exports = class TokenOptimizerService extends cds.ApplicationService {
24
+ init () {
25
+ this.on('summary', () => {
26
+ const s = optimizer.stats()
27
+ return { ...totals(s), since: s.since, mode: optimizer.config.mode, latencyMs: s.latencyMs, listenerErrors: s.listenerErrors }
28
+ })
29
+ this.on('READ', 'UseCases', (req) => rows(req, Object.entries(optimizer.stats().byUseCase).map(([name, t]) => ({ name, ...totals(t) })), 'name'))
30
+ this.on('READ', 'Models', (req) => rows(req, Object.entries(optimizer.stats().byModel).map(([name, t]) => ({ name, ...totals(t) })), 'name'))
31
+ this.on('READ', 'ErrorCategories', (req) => rows(req, Object.entries(optimizer.stats().errorsByCategory).map(([category, count]) => ({ category, count })), 'category'))
32
+ this.on('READ', 'StepSavings', (req) => rows(req, Object.entries(optimizer.stats().savingsByStep).map(([step, tokens]) => ({ step, tokens })), 'step'))
33
+ this.on('READ', 'PromptPrefixChanges', (req) => rows(req, Object.entries(optimizer.stats().promptPrefixChanges).map(([useCase, count]) => ({ useCase, count })), 'useCase'))
34
+ this.on('READ', 'Exporters', (req) => {
35
+ const s = optimizer.stats()
36
+ const names = optimizer.collector.exporters.map(x => x.name ?? 'anonymous')
37
+ return rows(req, names.map(name => ({ name, errors: s.exporterErrors[name] ?? 0, stats: s.exporters[name] ? JSON.stringify(s.exporters[name]) : null })), 'name')
38
+ })
39
+ return super.init()
40
+ }
41
+ }
@@ -0,0 +1,8 @@
1
+ using { cap.ai.token_optimizer as db } from '../db/token-usage-log';
2
+ using from './admin-service';
3
+
4
+ /** Added to the admin service when both the admin service and the db exporter are enabled. */
5
+ extend service TokenOptimizerService with {
6
+ @readonly
7
+ entity UsageLog as projection on db.TokenUsageLog;
8
+ }