@juspay/neurolink 9.94.7 → 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.
- package/CHANGELOG.md +6 -0
- package/dist/analytics/index.d.ts +7 -0
- package/dist/analytics/index.js +7 -0
- package/dist/analytics/parseQualityScore.d.ts +9 -0
- package/dist/analytics/parseQualityScore.js +29 -0
- package/dist/analytics/pricing.d.ts +24 -0
- package/dist/analytics/pricing.js +37 -0
- package/dist/analytics/service.d.ts +36 -0
- package/dist/analytics/service.js +393 -0
- package/dist/analytics/storage.d.ts +19 -0
- package/dist/analytics/storage.js +32 -0
- package/dist/browser/neurolink.min.js +389 -389
- package/dist/lib/analytics/index.d.ts +7 -0
- package/dist/lib/analytics/index.js +8 -0
- package/dist/lib/analytics/parseQualityScore.d.ts +9 -0
- package/dist/lib/analytics/parseQualityScore.js +30 -0
- package/dist/lib/analytics/pricing.d.ts +24 -0
- package/dist/lib/analytics/pricing.js +38 -0
- package/dist/lib/analytics/service.d.ts +36 -0
- package/dist/lib/analytics/service.js +394 -0
- package/dist/lib/analytics/storage.d.ts +19 -0
- package/dist/lib/analytics/storage.js +33 -0
- package/dist/lib/neurolink.d.ts +54 -0
- package/dist/lib/neurolink.js +302 -204
- package/dist/lib/types/analytics.d.ts +127 -0
- package/dist/neurolink.d.ts +54 -0
- package/dist/neurolink.js +302 -204
- package/dist/types/analytics.d.ts +127 -0
- package/package.json +2 -1
|
@@ -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,8 @@
|
|
|
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";
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -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,30 @@
|
|
|
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
|
+
}
|
|
30
|
+
//# sourceMappingURL=parseQualityScore.js.map
|
|
@@ -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,38 @@
|
|
|
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
|
+
}
|
|
38
|
+
//# sourceMappingURL=pricing.js.map
|
|
@@ -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,394 @@
|
|
|
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
|
+
}
|
|
394
|
+
//# sourceMappingURL=service.js.map
|
|
@@ -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,33 @@
|
|
|
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
|
+
}
|
|
33
|
+
//# sourceMappingURL=storage.js.map
|