aicodeswitch 5.2.11 → 5.2.13

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,377 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.ServicePerformanceTracker = void 0;
16
+ /**
17
+ * ServicePerformanceTracker - 服务性能统计全局聚合模块
18
+ *
19
+ * 设计要点(详见 docs/PRD/service-performance-tpm.md):
20
+ * - 全局统计,与 AUTH 模式无关;普通路由 + AccessKey 路由流量统一采集。
21
+ * - 两个数据点:TTFT(首 Token 返回时间)、TPM(每分钟吐 token 数)。
22
+ * - 三级聚合:供应商 → 服务 → 模型;上卷基于 sum+count 加权,avg 由 sum/count 派生。
23
+ * - 走势按小时桶(键 "YYYY-MM-DD HH",保留 72 桶)。
24
+ * - 内存增量 + debounce(5s) flush + 原子写(tmp+rename)。
25
+ *
26
+ * recordPerformance 为纯内存同步操作,可在请求完成路径无开销调用。
27
+ */
28
+ const path_1 = __importDefault(require("path"));
29
+ const promises_1 = __importDefault(require("fs/promises"));
30
+ const HOURLY_BUCKET_LIMIT = 72; // 保留最近 72 个小时桶(约 3 天)
31
+ class ServicePerformanceTracker {
32
+ constructor(dataPath) {
33
+ Object.defineProperty(this, "dataPath", {
34
+ enumerable: true,
35
+ configurable: true,
36
+ writable: true,
37
+ value: void 0
38
+ });
39
+ Object.defineProperty(this, "file", {
40
+ enumerable: true,
41
+ configurable: true,
42
+ writable: true,
43
+ value: { vendors: {} }
44
+ });
45
+ Object.defineProperty(this, "dirty", {
46
+ enumerable: true,
47
+ configurable: true,
48
+ writable: true,
49
+ value: false
50
+ });
51
+ Object.defineProperty(this, "flushTimer", {
52
+ enumerable: true,
53
+ configurable: true,
54
+ writable: true,
55
+ value: null
56
+ });
57
+ Object.defineProperty(this, "FLUSH_INTERVAL", {
58
+ enumerable: true,
59
+ configurable: true,
60
+ writable: true,
61
+ value: 5000
62
+ }); // 5s
63
+ this.dataPath = dataPath;
64
+ }
65
+ /** 初始化:加载已有数据文件 */
66
+ initialize() {
67
+ return __awaiter(this, void 0, void 0, function* () {
68
+ try {
69
+ const raw = yield promises_1.default.readFile(this.filePath, 'utf-8');
70
+ const parsed = JSON.parse(raw);
71
+ if (parsed && parsed.vendors)
72
+ this.file = parsed;
73
+ }
74
+ catch (_a) {
75
+ // 首次启动或文件损坏,使用空桶
76
+ this.file = { vendors: {} };
77
+ }
78
+ });
79
+ }
80
+ /**
81
+ * 记录一次请求的性能数据点(三级同步聚合)。
82
+ * 纯内存操作,不阻塞调用方。
83
+ */
84
+ recordPerformance(vendorId, vendorName, serviceId, serviceName, model, metrics, timestamp = Date.now()) {
85
+ if (!vendorId || !serviceId || !model)
86
+ return;
87
+ const hour = this.formatHourKey(timestamp);
88
+ // 模型级(最细,维护极值)
89
+ const modelAgg = this.ensureModel(vendorId, vendorName, serviceId, serviceName, model);
90
+ this.accumulate(modelAgg, metrics, hour, /* withExtremes */ true);
91
+ // 服务级上卷
92
+ const serviceAgg = this.ensureService(vendorId, vendorName, serviceId, serviceName);
93
+ this.accumulate(serviceAgg, metrics, hour, /* withExtremes */ false);
94
+ // 供应商级上卷
95
+ const vendorAgg = this.ensureVendor(vendorId, vendorName);
96
+ this.accumulate(vendorAgg, metrics, hour, /* withExtremes */ false);
97
+ this.dirty = true;
98
+ }
99
+ // ---------------- 读取(派生视图) ----------------
100
+ /** 全部供应商一览(vendorRollup 派生) */
101
+ getVendorsOverview() {
102
+ return Object.entries(this.file.vendors).map(([vendorId, v]) => ({
103
+ vendorId,
104
+ vendorName: v.vendorName,
105
+ derived: this.derive(v.vendorRollup),
106
+ }));
107
+ }
108
+ /** 全部 API 服务平铺一览(含所属供应商),用于「API 服务」维度对比 */
109
+ getServicesOverview() {
110
+ const out = [];
111
+ for (const [vendorId, v] of Object.entries(this.file.vendors)) {
112
+ for (const [serviceId, s] of Object.entries(v.services)) {
113
+ out.push({
114
+ serviceId,
115
+ serviceName: s.serviceName,
116
+ vendorId,
117
+ vendorName: v.vendorName,
118
+ derived: this.derive(s.serviceRollup),
119
+ });
120
+ }
121
+ }
122
+ return out;
123
+ }
124
+ /** 某供应商:自身 rollup + 其下所有服务 rollup */
125
+ getVendorDetail(vendorId) {
126
+ const v = this.file.vendors[vendorId];
127
+ if (!v)
128
+ return null;
129
+ return {
130
+ vendorName: v.vendorName,
131
+ derived: this.derive(v.vendorRollup),
132
+ hourly: this.trendFrom(vendorId, undefined, undefined),
133
+ services: Object.entries(v.services).map(([serviceId, s]) => ({
134
+ serviceId,
135
+ serviceName: s.serviceName,
136
+ derived: this.derive(s.serviceRollup),
137
+ })),
138
+ };
139
+ }
140
+ /** 某服务:自身 rollup + 其下所有模型 */
141
+ getServiceDetail(serviceId) {
142
+ const found = this.locateService(serviceId);
143
+ if (!found)
144
+ return null;
145
+ const { vendorId, vendorName, serviceEntry } = found;
146
+ return {
147
+ vendorId,
148
+ vendorName,
149
+ serviceName: serviceEntry.serviceName,
150
+ derived: this.derive(serviceEntry.serviceRollup),
151
+ hourly: this.trendFrom(vendorId, serviceId, undefined),
152
+ models: Object.entries(serviceEntry.models).map(([model, agg]) => ({
153
+ model,
154
+ derived: this.derive(agg),
155
+ })),
156
+ };
157
+ }
158
+ /** 单模型:派生 + 小时走势 + 极值 */
159
+ getModelDetail(serviceId, model) {
160
+ const found = this.locateService(serviceId);
161
+ if (!found)
162
+ return null;
163
+ const agg = found.serviceEntry.models[model];
164
+ if (!agg)
165
+ return null;
166
+ return {
167
+ derived: this.derive(agg),
168
+ hourly: this.trendFrom(found.vendorId, serviceId, model),
169
+ };
170
+ }
171
+ // ---------------- 持久化 ----------------
172
+ flush() {
173
+ return __awaiter(this, void 0, void 0, function* () {
174
+ if (!this.dirty)
175
+ return;
176
+ this.dirty = false;
177
+ yield this.save();
178
+ });
179
+ }
180
+ startAutoFlush() {
181
+ if (this.flushTimer)
182
+ return;
183
+ this.flushTimer = setInterval(() => {
184
+ this.flush().catch(err => console.error('[PerformanceTracker] Auto flush error:', err));
185
+ }, this.FLUSH_INTERVAL);
186
+ }
187
+ stopAutoFlush() {
188
+ if (this.flushTimer) {
189
+ clearInterval(this.flushTimer);
190
+ this.flushTimer = null;
191
+ }
192
+ }
193
+ get filePath() {
194
+ return path_1.default.join(this.dataPath, 'service-performance.json');
195
+ }
196
+ save() {
197
+ return __awaiter(this, void 0, void 0, function* () {
198
+ const tmp = this.filePath + '.tmp';
199
+ yield promises_1.default.mkdir(path_1.default.dirname(this.filePath), { recursive: true });
200
+ yield promises_1.default.writeFile(tmp, JSON.stringify(this.file, null, 2), 'utf-8');
201
+ yield promises_1.default.rename(tmp, this.filePath);
202
+ });
203
+ }
204
+ // ---------------- 内部:结构创建 ----------------
205
+ ensureVendor(vendorId, vendorName) {
206
+ let v = this.file.vendors[vendorId];
207
+ if (!v) {
208
+ v = {
209
+ vendorName,
210
+ vendorRollup: this.emptyAggregate(),
211
+ services: {},
212
+ };
213
+ this.file.vendors[vendorId] = v;
214
+ }
215
+ if (vendorName && !v.vendorName)
216
+ v.vendorName = vendorName;
217
+ return v.vendorRollup;
218
+ }
219
+ ensureService(vendorId, vendorName, serviceId, serviceName) {
220
+ var _a;
221
+ const v = (_a = this.file.vendors[vendorId]) !== null && _a !== void 0 ? _a : (this.file.vendors[vendorId] = {
222
+ vendorName, vendorRollup: this.emptyAggregate(), services: {},
223
+ });
224
+ let s = v.services[serviceId];
225
+ if (!s) {
226
+ s = {
227
+ serviceName,
228
+ serviceRollup: this.emptyAggregate(),
229
+ models: {},
230
+ updatedAt: Date.now(),
231
+ };
232
+ v.services[serviceId] = s;
233
+ }
234
+ if (serviceName && !s.serviceName)
235
+ s.serviceName = serviceName;
236
+ s.updatedAt = Date.now();
237
+ return s.serviceRollup;
238
+ }
239
+ ensureModel(vendorId, vendorName, serviceId, serviceName, model) {
240
+ // 确保供应商 + 服务节点存在(service rollup 由 recordPerformance 单独累加)
241
+ this.ensureService(vendorId, vendorName, serviceId, serviceName);
242
+ const s = this.file.vendors[vendorId].services[serviceId];
243
+ let m = s.models[model];
244
+ if (!m) {
245
+ m = this.emptyAggregate();
246
+ s.models[model] = m;
247
+ }
248
+ return m;
249
+ }
250
+ // ---------------- 内部:累加 ----------------
251
+ emptyAggregate() {
252
+ return {
253
+ precise: this.emptyBucket(),
254
+ estimated: this.emptyBucket(),
255
+ errorCount: 0,
256
+ hourly: {},
257
+ };
258
+ }
259
+ emptyBucket() {
260
+ return { count: 0, sumTtftMs: 0, sumTps: 0, totalOutputTokens: 0 };
261
+ }
262
+ accumulate(agg, m, hour, withExtremes) {
263
+ var _a;
264
+ if (m.isError) {
265
+ agg.errorCount += 1;
266
+ return;
267
+ }
268
+ const bucket = m.timingAccuracy === 'precise' ? agg.precise : agg.estimated;
269
+ const hasTtft = m.timingAccuracy === 'precise' && typeof m.ttftMs === 'number';
270
+ const hasTps = typeof m.tokensPerSecond === 'number';
271
+ bucket.count += 1;
272
+ if (hasTtft)
273
+ bucket.sumTtftMs += m.ttftMs;
274
+ if (hasTps)
275
+ bucket.sumTps += m.tokensPerSecond;
276
+ if (m.outputTokens)
277
+ bucket.totalOutputTokens += m.outputTokens;
278
+ // 小时桶(仅精确样本计入走势,避免估算样本污染)
279
+ if (m.timingAccuracy === 'precise') {
280
+ const hb = (_a = agg.hourly[hour]) !== null && _a !== void 0 ? _a : (agg.hourly[hour] = this.emptyBucket());
281
+ hb.count += 1;
282
+ if (hasTtft)
283
+ hb.sumTtftMs += m.ttftMs;
284
+ if (hasTps)
285
+ hb.sumTps += m.tokensPerSecond;
286
+ if (m.outputTokens)
287
+ hb.totalOutputTokens += m.outputTokens;
288
+ this.trimHourly(agg.hourly);
289
+ }
290
+ // 极值(仅模型级、仅精确样本)
291
+ if (withExtremes && m.timingAccuracy === 'precise') {
292
+ if (hasTtft) {
293
+ if (agg.minTtftMs === undefined || m.ttftMs < agg.minTtftMs)
294
+ agg.minTtftMs = m.ttftMs;
295
+ if (agg.maxTtftMs === undefined || m.ttftMs > agg.maxTtftMs)
296
+ agg.maxTtftMs = m.ttftMs;
297
+ }
298
+ if (hasTps) {
299
+ if (agg.minTps === undefined || m.tokensPerSecond < agg.minTps)
300
+ agg.minTps = m.tokensPerSecond;
301
+ if (agg.maxTps === undefined || m.tokensPerSecond > agg.maxTps)
302
+ agg.maxTps = m.tokensPerSecond;
303
+ }
304
+ }
305
+ }
306
+ trimHourly(hourly) {
307
+ const keys = Object.keys(hourly);
308
+ if (keys.length <= HOURLY_BUCKET_LIMIT)
309
+ return;
310
+ keys.sort(); // "YYYY-MM-DD HH" 字典序即时间序
311
+ const drop = keys.length - HOURLY_BUCKET_LIMIT;
312
+ for (let i = 0; i < drop; i++)
313
+ delete hourly[keys[i]];
314
+ }
315
+ // ---------------- 内部:派生 ----------------
316
+ derive(agg) {
317
+ const p = agg.precise;
318
+ const count = p.count;
319
+ const avgTtftMs = count > 0 ? p.sumTtftMs / count : 0;
320
+ const avgTps = count > 0 ? p.sumTps / count : 0;
321
+ return {
322
+ count,
323
+ avgTtftMs,
324
+ avgTpm: avgTps * 60,
325
+ minTtftMs: agg.minTtftMs,
326
+ maxTtftMs: agg.maxTtftMs,
327
+ minTps: agg.minTps,
328
+ maxTps: agg.maxTps,
329
+ errorCount: agg.errorCount,
330
+ totalOutputTokens: p.totalOutputTokens,
331
+ successRate: count + agg.errorCount > 0 ? count / (count + agg.errorCount) : 0,
332
+ };
333
+ }
334
+ trendFrom(vendorId, serviceId, model) {
335
+ var _a, _b;
336
+ const v = this.file.vendors[vendorId];
337
+ if (!v)
338
+ return [];
339
+ let agg;
340
+ if (model && serviceId) {
341
+ agg = (_a = v.services[serviceId]) === null || _a === void 0 ? void 0 : _a.models[model];
342
+ }
343
+ else if (serviceId) {
344
+ agg = (_b = v.services[serviceId]) === null || _b === void 0 ? void 0 : _b.serviceRollup;
345
+ }
346
+ else {
347
+ agg = v.vendorRollup;
348
+ }
349
+ if (!agg)
350
+ return [];
351
+ return Object.entries(agg.hourly)
352
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
353
+ .map(([hour, b]) => ({
354
+ hour,
355
+ count: b.count,
356
+ avgTtftMs: b.count > 0 ? b.sumTtftMs / b.count : 0,
357
+ avgTpm: b.count > 0 ? (b.sumTps / b.count) * 60 : 0,
358
+ }));
359
+ }
360
+ locateService(serviceId) {
361
+ for (const [vendorId, v] of Object.entries(this.file.vendors)) {
362
+ const s = v.services[serviceId];
363
+ if (s)
364
+ return { vendorId, vendorName: v.vendorName, serviceEntry: s };
365
+ }
366
+ return null;
367
+ }
368
+ formatHourKey(ts) {
369
+ const d = new Date(ts);
370
+ const yyyy = d.getFullYear();
371
+ const mm = String(d.getMonth() + 1).padStart(2, '0');
372
+ const dd = String(d.getDate()).padStart(2, '0');
373
+ const hh = String(d.getHours()).padStart(2, '0');
374
+ return `${yyyy}-${mm}-${dd} ${hh}`;
375
+ }
376
+ }
377
+ exports.ServicePerformanceTracker = ServicePerformanceTracker;