@juspay/neurolink 9.94.6 → 9.95.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.
Files changed (34) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/analytics/index.d.ts +7 -0
  3. package/dist/analytics/index.js +7 -0
  4. package/dist/analytics/parseQualityScore.d.ts +9 -0
  5. package/dist/analytics/parseQualityScore.js +29 -0
  6. package/dist/analytics/pricing.d.ts +24 -0
  7. package/dist/analytics/pricing.js +37 -0
  8. package/dist/analytics/service.d.ts +36 -0
  9. package/dist/analytics/service.js +393 -0
  10. package/dist/analytics/storage.d.ts +19 -0
  11. package/dist/analytics/storage.js +32 -0
  12. package/dist/browser/neurolink.min.js +389 -389
  13. package/dist/cli/factories/commandFactory.d.ts +0 -2
  14. package/dist/cli/factories/commandFactory.js +36 -45
  15. package/dist/cli/loop/session.js +21 -0
  16. package/dist/cli/utils/inputValidation.d.ts +38 -0
  17. package/dist/cli/utils/inputValidation.js +143 -0
  18. package/dist/lib/analytics/index.d.ts +7 -0
  19. package/dist/lib/analytics/index.js +8 -0
  20. package/dist/lib/analytics/parseQualityScore.d.ts +9 -0
  21. package/dist/lib/analytics/parseQualityScore.js +30 -0
  22. package/dist/lib/analytics/pricing.d.ts +24 -0
  23. package/dist/lib/analytics/pricing.js +38 -0
  24. package/dist/lib/analytics/service.d.ts +36 -0
  25. package/dist/lib/analytics/service.js +394 -0
  26. package/dist/lib/analytics/storage.d.ts +19 -0
  27. package/dist/lib/analytics/storage.js +33 -0
  28. package/dist/lib/neurolink.d.ts +54 -0
  29. package/dist/lib/neurolink.js +302 -204
  30. package/dist/lib/types/analytics.d.ts +127 -0
  31. package/dist/neurolink.d.ts +54 -0
  32. package/dist/neurolink.js +302 -204
  33. package/dist/types/analytics.d.ts +127 -0
  34. package/package.json +2 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## [9.95.0](https://github.com/juspay/neurolink/compare/v9.94.7...v9.95.0) (2026-07-19)
2
+
3
+ ### Features
4
+
5
+ - **(analytics):** add production-ready advanced analytics SDK methods ([374dad7](https://github.com/juspay/neurolink/commit/374dad73debc2accfc8d6523d631335ac1365d43))
6
+
7
+ ## [9.94.7](https://github.com/juspay/neurolink/compare/v9.94.6...v9.94.7) (2026-07-19)
8
+
9
+ ### Bug Fixes
10
+
11
+ - **(cli):** validate csv-max-rows, warn on large files, and document multimodal inputs ([124f0d6](https://github.com/juspay/neurolink/commit/124f0d6ca95d7d0819ab001cb9cf753052308ed1))
12
+
1
13
  ## [9.94.6](https://github.com/juspay/neurolink/compare/v9.94.5...v9.94.6) (2026-07-19)
2
14
 
3
15
  ### Bug Fixes
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Analytics Module Exports (runtime values only — types live in src/lib/types)
3
+ */
4
+ export { calculateAdvancedCost } from "./pricing.js";
5
+ export { InMemoryAnalyticsStorage, DEFAULT_ANALYTICS_STORAGE_MAX_RECORDS, } from "./storage.js";
6
+ export { AnalyticsService } from "./service.js";
7
+ export { parseAnalyticsQualityScore } from "./parseQualityScore.js";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Analytics Module Exports (runtime values only — types live in src/lib/types)
3
+ */
4
+ export { calculateAdvancedCost } from "./pricing.js";
5
+ export { InMemoryAnalyticsStorage, DEFAULT_ANALYTICS_STORAGE_MAX_RECORDS, } from "./storage.js";
6
+ export { AnalyticsService } from "./service.js";
7
+ export { parseAnalyticsQualityScore } from "./parseQualityScore.js";
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Safe narrowing of unknown event payloads into AnalyticsQualityScore.
3
+ */
4
+ import type { AnalyticsQualityScore } from "../types/index.js";
5
+ /**
6
+ * Convert an unknown quality-score payload into AnalyticsQualityScore.
7
+ * Returns undefined for missing, null, or malformed values.
8
+ */
9
+ export declare function parseAnalyticsQualityScore(value: unknown): AnalyticsQualityScore | undefined;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Safe narrowing of unknown event payloads into AnalyticsQualityScore.
3
+ */
4
+ /**
5
+ * Convert an unknown quality-score payload into AnalyticsQualityScore.
6
+ * Returns undefined for missing, null, or malformed values.
7
+ */
8
+ export function parseAnalyticsQualityScore(value) {
9
+ if (!value || typeof value !== "object") {
10
+ return undefined;
11
+ }
12
+ const record = value;
13
+ // Reject NaN/Infinity — typeof NaN === "number"
14
+ if (!Number.isFinite(record.overall) ||
15
+ !Number.isFinite(record.relevance) ||
16
+ !Number.isFinite(record.accuracy) ||
17
+ !Number.isFinite(record.completeness)) {
18
+ return undefined;
19
+ }
20
+ return {
21
+ overall: record.overall,
22
+ relevance: record.relevance,
23
+ accuracy: record.accuracy,
24
+ completeness: record.completeness,
25
+ ...(typeof record.reasoning === "string"
26
+ ? { reasoning: record.reasoning }
27
+ : {}),
28
+ };
29
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Analytics cost helpers.
3
+ *
4
+ * Prefer costs already computed by the telemetry/pricing pipeline
5
+ * (`utils/pricing.calculateCost` / span `ai.cost.total`). This module
6
+ * is a thin compatibility wrapper — it does not maintain a second
7
+ * hardcoded pricing table.
8
+ */
9
+ /**
10
+ * Calculate cost from token counts using the canonical SDK pricing table.
11
+ * Returns 0 when the provider/model combination is unknown.
12
+ *
13
+ * Partial model-name matching uses longest-prefix preference inside
14
+ * `calculateCost` / `findRates` — shorter keys (e.g. `gpt-4o`) never
15
+ * win over more specific ones (e.g. `gpt-4o-mini`).
16
+ *
17
+ * @param model - Model name
18
+ * @param inputTokens - Number of input tokens
19
+ * @param outputTokens - Number of output tokens
20
+ * @param provider - Provider name (improves rate lookup; defaults to
21
+ * cross-provider search via openrouter alias)
22
+ * @returns Total cost in USD
23
+ */
24
+ export declare function calculateAdvancedCost(model: string | undefined, inputTokens: number, outputTokens: number, provider?: string): number;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Analytics cost helpers.
3
+ *
4
+ * Prefer costs already computed by the telemetry/pricing pipeline
5
+ * (`utils/pricing.calculateCost` / span `ai.cost.total`). This module
6
+ * is a thin compatibility wrapper — it does not maintain a second
7
+ * hardcoded pricing table.
8
+ */
9
+ import { calculateCost } from "../utils/pricing.js";
10
+ /**
11
+ * Calculate cost from token counts using the canonical SDK pricing table.
12
+ * Returns 0 when the provider/model combination is unknown.
13
+ *
14
+ * Partial model-name matching uses longest-prefix preference inside
15
+ * `calculateCost` / `findRates` — shorter keys (e.g. `gpt-4o`) never
16
+ * win over more specific ones (e.g. `gpt-4o-mini`).
17
+ *
18
+ * @param model - Model name
19
+ * @param inputTokens - Number of input tokens
20
+ * @param outputTokens - Number of output tokens
21
+ * @param provider - Provider name (improves rate lookup; defaults to
22
+ * cross-provider search via openrouter alias)
23
+ * @returns Total cost in USD
24
+ */
25
+ export function calculateAdvancedCost(model, inputTokens, outputTokens, provider) {
26
+ if (!model) {
27
+ return 0;
28
+ }
29
+ // When provider is unknown, use the openrouter/litellm cross-provider
30
+ // search path in findRates (PROVIDER_ALIASES → __cross_provider__).
31
+ const resolvedProvider = provider && provider.length > 0 ? provider : "openrouter";
32
+ return calculateCost(resolvedProvider, model, {
33
+ input: inputTokens,
34
+ output: outputTokens,
35
+ total: inputTokens + outputTokens,
36
+ });
37
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Advanced Analytics Service Layer
3
+ * Handles metrics collection, aggregation, cost analysis, and projections.
4
+ */
5
+ import type { AnalyticsStorage, CostAnalysisOptions, CostAnalysisResult, ProviderMetricsOptions, ProviderMetricsResult, TeamAnalyticsOptions, TeamAnalyticsResult, TelemetryRecord } from "../types/index.js";
6
+ export declare class AnalyticsService {
7
+ private storage;
8
+ constructor(storage?: AnalyticsStorage);
9
+ /**
10
+ * Helper to parse dynamic time range formats safely
11
+ */
12
+ private parseTimeRange;
13
+ /**
14
+ * Capture a request lifecycle record
15
+ */
16
+ trackRequest(record: Omit<TelemetryRecord, "id" | "cost"> & {
17
+ cost?: number;
18
+ }): Promise<void>;
19
+ /**
20
+ * getProviderMetrics implementation
21
+ */
22
+ getProviderMetrics(options?: ProviderMetricsOptions): Promise<ProviderMetricsResult>;
23
+ /**
24
+ * Helper to format timestamps for date groupings.
25
+ * Clones the Date so week arithmetic never mutates the caller's value.
26
+ */
27
+ private formatGroupDate;
28
+ /**
29
+ * getCostAnalysis implementation
30
+ */
31
+ getCostAnalysis(options?: CostAnalysisOptions): Promise<CostAnalysisResult>;
32
+ /**
33
+ * getTeamAnalytics implementation
34
+ */
35
+ getTeamAnalytics(options?: TeamAnalyticsOptions): Promise<TeamAnalyticsResult>;
36
+ }
@@ -0,0 +1,393 @@
1
+ /**
2
+ * Advanced Analytics Service Layer
3
+ * Handles metrics collection, aggregation, cost analysis, and projections.
4
+ */
5
+ import { calculateAdvancedCost } from "./pricing.js";
6
+ import { InMemoryAnalyticsStorage } from "./storage.js";
7
+ export class AnalyticsService {
8
+ storage;
9
+ constructor(storage) {
10
+ this.storage = storage || new InMemoryAnalyticsStorage();
11
+ }
12
+ /**
13
+ * Helper to parse dynamic time range formats safely
14
+ */
15
+ parseTimeRange(timeRange) {
16
+ if (!timeRange) {
17
+ return null;
18
+ }
19
+ if (typeof timeRange === "object" &&
20
+ "start" in timeRange &&
21
+ "end" in timeRange) {
22
+ const startObj = timeRange.start;
23
+ const endObj = timeRange.end;
24
+ const start = startObj instanceof Date
25
+ ? startObj.getTime()
26
+ : new Date(startObj).getTime();
27
+ const end = endObj instanceof Date
28
+ ? endObj.getTime()
29
+ : new Date(endObj).getTime();
30
+ if (!Number.isNaN(start) && !Number.isNaN(end)) {
31
+ return { start, end };
32
+ }
33
+ }
34
+ if (typeof timeRange === "string") {
35
+ const now = Date.now();
36
+ const dayMs = 24 * 3600 * 1000;
37
+ if (timeRange === "last_24_hours") {
38
+ return { start: now - dayMs, end: now };
39
+ }
40
+ if (timeRange === "last_7_days") {
41
+ return { start: now - 7 * dayMs, end: now };
42
+ }
43
+ if (timeRange === "last_30_days") {
44
+ return { start: now - 30 * dayMs, end: now };
45
+ }
46
+ // Month/quarter bounds use UTC midnight for timezone-stable windows
47
+ if (timeRange === "current_month") {
48
+ const d = new Date();
49
+ const startUtc = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1);
50
+ return { start: startUtc, end: now };
51
+ }
52
+ if (timeRange === "current_quarter") {
53
+ const d = new Date();
54
+ const qMonth = Math.floor(d.getUTCMonth() / 3) * 3;
55
+ const startUtc = Date.UTC(d.getUTCFullYear(), qMonth, 1);
56
+ return { start: startUtc, end: now };
57
+ }
58
+ }
59
+ return null;
60
+ }
61
+ /**
62
+ * Capture a request lifecycle record
63
+ */
64
+ async trackRequest(record) {
65
+ // Preserve explicit costs (including 0). Only price when cost is omitted.
66
+ const cost = record.cost !== undefined
67
+ ? record.cost
68
+ : calculateAdvancedCost(record.model, record.inputTokens, record.outputTokens, record.provider);
69
+ const fullRecord = {
70
+ ...record,
71
+ id: crypto.randomUUID(),
72
+ cost,
73
+ };
74
+ await this.storage.saveRecord(fullRecord);
75
+ }
76
+ /**
77
+ * getProviderMetrics implementation
78
+ */
79
+ async getProviderMetrics(options = {}) {
80
+ const allRecords = await this.storage.getRecords();
81
+ const range = this.parseTimeRange(options.timeRange);
82
+ // Filter records
83
+ const records = allRecords.filter((r) => {
84
+ if (range && (r.timestamp < range.start || r.timestamp > range.end)) {
85
+ return false;
86
+ }
87
+ if (options.providers && options.providers.length > 0) {
88
+ if (!options.providers.includes(r.provider)) {
89
+ return false;
90
+ }
91
+ }
92
+ return true;
93
+ });
94
+ // Compute top-level aggregated metrics
95
+ let totalLatency = 0;
96
+ let totalTokens = 0;
97
+ let inputTokens = 0;
98
+ let outputTokens = 0;
99
+ let errors = 0;
100
+ let totalCost = 0;
101
+ const requestCount = records.length;
102
+ // Group by provider
103
+ const providerMap = {};
104
+ for (const r of records) {
105
+ totalLatency += r.latency;
106
+ totalTokens += r.totalTokens;
107
+ inputTokens += r.inputTokens;
108
+ outputTokens += r.outputTokens;
109
+ if (r.isError) {
110
+ errors++;
111
+ }
112
+ totalCost += r.cost;
113
+ if (!providerMap[r.provider]) {
114
+ providerMap[r.provider] = [];
115
+ }
116
+ providerMap[r.provider].push(r);
117
+ }
118
+ const averageLatency = requestCount > 0 ? totalLatency / requestCount : 0;
119
+ const errorRate = requestCount > 0 ? errors / requestCount : 0;
120
+ // Empty datasets must not report a fabricated 100% success rate
121
+ const successRate = requestCount > 0 ? 1 - errorRate : 0;
122
+ const costPerToken = totalTokens > 0 ? totalCost / totalTokens : 0;
123
+ // Build per-provider results
124
+ const providers = [];
125
+ for (const [name, pRecords] of Object.entries(providerMap)) {
126
+ const pCount = pRecords.length;
127
+ let pLatency = 0;
128
+ let pTotalTokens = 0;
129
+ let pInputTokens = 0;
130
+ let pOutputTokens = 0;
131
+ let pErrors = 0;
132
+ let pCost = 0;
133
+ for (const r of pRecords) {
134
+ pLatency += r.latency;
135
+ pTotalTokens += r.totalTokens;
136
+ pInputTokens += r.inputTokens;
137
+ pOutputTokens += r.outputTokens;
138
+ if (r.isError) {
139
+ pErrors++;
140
+ }
141
+ pCost += r.cost;
142
+ }
143
+ const pAvgLatency = pCount > 0 ? pLatency / pCount : 0;
144
+ const pErrorRate = pCount > 0 ? pErrors / pCount : 0;
145
+ const pSuccessRate = 1 - pErrorRate;
146
+ const pCostPerToken = pTotalTokens > 0 ? pCost / pTotalTokens : 0;
147
+ providers.push({
148
+ name,
149
+ averageLatency: pAvgLatency,
150
+ averageResponseTime: pAvgLatency, // docs compatibility
151
+ totalTokens: pTotalTokens,
152
+ inputTokens: pInputTokens,
153
+ outputTokens: pOutputTokens,
154
+ errorRate: pErrorRate,
155
+ successRate: pSuccessRate,
156
+ costPerToken: pCostPerToken,
157
+ totalCost: pCost,
158
+ requestCount: pCount,
159
+ });
160
+ }
161
+ return {
162
+ averageLatency,
163
+ averageResponseTime: averageLatency,
164
+ totalTokens,
165
+ inputTokens,
166
+ outputTokens,
167
+ errorRate,
168
+ successRate,
169
+ costPerToken,
170
+ totalCost,
171
+ requestCount,
172
+ providers,
173
+ };
174
+ }
175
+ /**
176
+ * Helper to format timestamps for date groupings.
177
+ * Clones the Date so week arithmetic never mutates the caller's value.
178
+ */
179
+ formatGroupDate(timestamp, type) {
180
+ const d = new Date(timestamp);
181
+ if (type === "day") {
182
+ return d.toISOString().split("T")[0];
183
+ }
184
+ if (type === "month") {
185
+ return d.toISOString().substring(0, 7);
186
+ }
187
+ if (type === "week") {
188
+ // UTC Monday of the containing ISO week — never mutate caller input
189
+ // and avoid local/UTC drift from mixing getDay()/toISOString().
190
+ const clone = new Date(timestamp);
191
+ const day = clone.getUTCDay();
192
+ const diff = clone.getUTCDate() - day + (day === 0 ? -6 : 1);
193
+ clone.setUTCDate(diff);
194
+ return clone.toISOString().split("T")[0];
195
+ }
196
+ return d.toISOString().split("T")[0];
197
+ }
198
+ /**
199
+ * getCostAnalysis implementation
200
+ */
201
+ async getCostAnalysis(options = {}) {
202
+ const allRecords = await this.storage.getRecords();
203
+ const range = this.parseTimeRange(options.timeRange);
204
+ const records = allRecords.filter((r) => {
205
+ if (range && (r.timestamp < range.start || r.timestamp > range.end)) {
206
+ return false;
207
+ }
208
+ return true;
209
+ });
210
+ let totalCost = 0;
211
+ let totalTokens = 0;
212
+ let inputTokens = 0;
213
+ let outputTokens = 0;
214
+ const requestCount = records.length;
215
+ const groups = {};
216
+ const providerGroupsMap = {};
217
+ const groupByArr = Array.isArray(options.groupBy)
218
+ ? options.groupBy
219
+ : typeof options.groupBy === "string"
220
+ ? [options.groupBy]
221
+ : ["provider"];
222
+ for (const r of records) {
223
+ totalCost += r.cost;
224
+ totalTokens += r.totalTokens;
225
+ inputTokens += r.inputTokens;
226
+ outputTokens += r.outputTokens;
227
+ // Ensure per-provider items are always built for documentation support
228
+ if (!providerGroupsMap[r.provider]) {
229
+ providerGroupsMap[r.provider] = {
230
+ groupKey: r.provider,
231
+ provider: r.provider,
232
+ totalCost: 0,
233
+ costPerToken: 0,
234
+ inputTokens: 0,
235
+ outputTokens: 0,
236
+ totalTokens: 0,
237
+ requestCount: 0,
238
+ };
239
+ }
240
+ const pg = providerGroupsMap[r.provider];
241
+ pg.totalCost += r.cost;
242
+ pg.inputTokens += r.inputTokens;
243
+ pg.outputTokens += r.outputTokens;
244
+ pg.totalTokens += r.totalTokens;
245
+ pg.requestCount += 1;
246
+ pg.costPerToken = pg.totalTokens > 0 ? pg.totalCost / pg.totalTokens : 0;
247
+ // Build specific requested group key
248
+ const keyParts = [];
249
+ for (const g of groupByArr) {
250
+ if (g === "provider") {
251
+ keyParts.push(r.provider);
252
+ }
253
+ else if (g === "model") {
254
+ keyParts.push(r.model);
255
+ }
256
+ else if (g === "user_id" || g === "userId") {
257
+ keyParts.push(r.userId || "unknown");
258
+ }
259
+ else if (g === "day" || g === "week" || g === "month") {
260
+ keyParts.push(this.formatGroupDate(r.timestamp, g));
261
+ }
262
+ else {
263
+ keyParts.push(r.provider);
264
+ }
265
+ }
266
+ const groupKey = keyParts.join("#") || r.provider;
267
+ if (!groups[groupKey]) {
268
+ groups[groupKey] = {
269
+ groupKey,
270
+ provider: r.provider,
271
+ model: r.model,
272
+ userId: r.userId,
273
+ totalCost: 0,
274
+ costPerToken: 0,
275
+ inputTokens: 0,
276
+ outputTokens: 0,
277
+ totalTokens: 0,
278
+ requestCount: 0,
279
+ };
280
+ }
281
+ const gItem = groups[groupKey];
282
+ gItem.totalCost += r.cost;
283
+ gItem.inputTokens += r.inputTokens;
284
+ gItem.outputTokens += r.outputTokens;
285
+ gItem.totalTokens += r.totalTokens;
286
+ gItem.requestCount += 1;
287
+ gItem.costPerToken =
288
+ gItem.totalTokens > 0 ? gItem.totalCost / gItem.totalTokens : 0;
289
+ }
290
+ const providers = Object.values(providerGroupsMap);
291
+ // Optional future projections using simple average-based estimation
292
+ let projections;
293
+ if (options.includeProjections) {
294
+ if (records.length === 0) {
295
+ projections = { nextMonth: 0, nextQuarter: 0 };
296
+ }
297
+ else {
298
+ // Find span of records in days
299
+ const timestamps = records.map((r) => r.timestamp);
300
+ const minTime = Math.min(...timestamps);
301
+ const maxTime = Math.max(...timestamps);
302
+ const diffDays = Math.max(1, (maxTime - minTime) / (24 * 3600 * 1000));
303
+ const dailyAverage = totalCost / diffDays;
304
+ projections = {
305
+ nextMonth: dailyAverage * 30,
306
+ nextQuarter: dailyAverage * 90,
307
+ };
308
+ }
309
+ }
310
+ return {
311
+ totalCost,
312
+ totalTokens,
313
+ inputTokens,
314
+ outputTokens,
315
+ requestCount,
316
+ groups,
317
+ providers,
318
+ projections,
319
+ };
320
+ }
321
+ /**
322
+ * getTeamAnalytics implementation
323
+ */
324
+ async getTeamAnalytics(options = {}) {
325
+ const allRecords = await this.storage.getRecords();
326
+ const range = this.parseTimeRange(options.timeRange);
327
+ // teamId and departments are independent AND filters (docs: both supported).
328
+ const records = allRecords.filter((r) => {
329
+ if (range && (r.timestamp < range.start || r.timestamp > range.end)) {
330
+ return false;
331
+ }
332
+ if (options.teamId) {
333
+ const recTeam = r.teamId || "default";
334
+ if (recTeam !== options.teamId) {
335
+ return false;
336
+ }
337
+ }
338
+ if (options.departments && options.departments.length > 0) {
339
+ const dept = r.department;
340
+ if (!dept || !options.departments.includes(dept)) {
341
+ return false;
342
+ }
343
+ }
344
+ return true;
345
+ });
346
+ const totalRequests = records.length;
347
+ const uniqueUsersSet = new Set();
348
+ const providersSet = new Set();
349
+ const costBreakdownByProvider = {};
350
+ const costBreakdownByUser = {};
351
+ let totalQualityScore = 0;
352
+ let totalRelevance = 0;
353
+ let totalAccuracy = 0;
354
+ let totalCompleteness = 0;
355
+ let scoredCount = 0;
356
+ for (const r of records) {
357
+ if (r.userId) {
358
+ uniqueUsersSet.add(r.userId);
359
+ }
360
+ providersSet.add(r.provider);
361
+ costBreakdownByProvider[r.provider] =
362
+ (costBreakdownByProvider[r.provider] || 0) + r.cost;
363
+ const uKey = r.userId || "anonymous";
364
+ costBreakdownByUser[uKey] = (costBreakdownByUser[uKey] || 0) + r.cost;
365
+ if (r.qualityScore) {
366
+ scoredCount++;
367
+ totalQualityScore += r.qualityScore.overall;
368
+ totalRelevance += r.qualityScore.relevance;
369
+ totalAccuracy += r.qualityScore.accuracy;
370
+ totalCompleteness += r.qualityScore.completeness;
371
+ }
372
+ }
373
+ // Omit fabricated scores when no requests were evaluated
374
+ const qualityScores = scoredCount > 0
375
+ ? {
376
+ overall: totalQualityScore / scoredCount,
377
+ relevance: totalRelevance / scoredCount,
378
+ accuracy: totalAccuracy / scoredCount,
379
+ completeness: totalCompleteness / scoredCount,
380
+ reasoning: "Aggregated automated response evaluations",
381
+ }
382
+ : undefined;
383
+ return {
384
+ totalRequests,
385
+ // Count only attributed users — never fabricate a user when none present
386
+ uniqueUsers: uniqueUsersSet.size,
387
+ providersUsed: Array.from(providersSet),
388
+ costBreakdownByProvider,
389
+ costBreakdownByUser,
390
+ qualityScores,
391
+ };
392
+ }
393
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Analytics storage — in-memory implementation with bounded FIFO capacity.
3
+ * Designed modularly so Redis or a database can later replace the backend.
4
+ */
5
+ import type { AnalyticsStorage, InMemoryAnalyticsStorageOptions, TelemetryRecord } from "../types/index.js";
6
+ /** Default max records retained in memory (FIFO eviction beyond this). */
7
+ export declare const DEFAULT_ANALYTICS_STORAGE_MAX_RECORDS = 10000;
8
+ /**
9
+ * Lightweight bounded in-memory storage implementation.
10
+ * Public surface of AnalyticsStorage is unchanged; maxRecords is optional.
11
+ */
12
+ export declare class InMemoryAnalyticsStorage implements AnalyticsStorage {
13
+ private records;
14
+ private readonly maxRecords;
15
+ constructor(options?: InMemoryAnalyticsStorageOptions);
16
+ saveRecord(record: TelemetryRecord): Promise<void>;
17
+ getRecords(): Promise<TelemetryRecord[]>;
18
+ clear(): Promise<void>;
19
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Analytics storage — in-memory implementation with bounded FIFO capacity.
3
+ * Designed modularly so Redis or a database can later replace the backend.
4
+ */
5
+ /** Default max records retained in memory (FIFO eviction beyond this). */
6
+ export const DEFAULT_ANALYTICS_STORAGE_MAX_RECORDS = 10_000;
7
+ /**
8
+ * Lightweight bounded in-memory storage implementation.
9
+ * Public surface of AnalyticsStorage is unchanged; maxRecords is optional.
10
+ */
11
+ export class InMemoryAnalyticsStorage {
12
+ records = [];
13
+ maxRecords;
14
+ constructor(options) {
15
+ const max = options?.maxRecords ?? DEFAULT_ANALYTICS_STORAGE_MAX_RECORDS;
16
+ this.maxRecords =
17
+ max > 0 ? Math.floor(max) : DEFAULT_ANALYTICS_STORAGE_MAX_RECORDS;
18
+ }
19
+ async saveRecord(record) {
20
+ this.records.push(record);
21
+ // Slice once when over capacity — avoids O(n²) repeated shift() on burst.
22
+ if (this.records.length > this.maxRecords) {
23
+ this.records = this.records.slice(-this.maxRecords);
24
+ }
25
+ }
26
+ async getRecords() {
27
+ return [...this.records];
28
+ }
29
+ async clear() {
30
+ this.records = [];
31
+ }
32
+ }